Repository: fordes123/subtitles-view Branch: main Commit: f7a97b834a66 Files: 155 Total size: 358.4 KB Directory structure: gitextract__0rcvlyu/ ├── .github/ │ └── workflows/ │ └── subtitles-view.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src/ └── main/ ├── java/ │ └── org/ │ └── fordes/ │ └── subtitles/ │ └── view/ │ ├── SubtitlesViewApplication.java │ ├── config/ │ │ ├── ApplicationConfig.java │ │ └── ExecutorConfig.java │ ├── constant/ │ │ ├── CommonConstant.java │ │ └── StyleClassConstant.java │ ├── controller/ │ │ ├── DelayInitController.java │ │ ├── EditTool.java │ │ ├── Export.java │ │ ├── MainController.java │ │ ├── MainEditor.java │ │ ├── QuickStart.java │ │ ├── Setting.java │ │ ├── SidebarAfter.java │ │ ├── SidebarBefore.java │ │ ├── SidebarBottom.java │ │ ├── SpeechConversion.java │ │ ├── SubtitleSearch.java │ │ ├── SyncEditor.java │ │ ├── TitleBar.java │ │ ├── Toast.java │ │ ├── ToolBox.java │ │ └── VoiceConvert.java │ ├── enums/ │ │ ├── EditToolEventEnum.java │ │ ├── FileEnum.java │ │ ├── FontIcon.java │ │ ├── ServiceProvider.java │ │ ├── ServiceType.java │ │ └── SevenZipEnum.java │ ├── event/ │ │ ├── AbstractToastEvent.java │ │ ├── EditToolEvent.java │ │ ├── FileOpenEvent.java │ │ ├── LoadingEvent.java │ │ ├── ThemeChangeEvent.java │ │ ├── ToastChooseEvent.java │ │ ├── ToastConfirmEvent.java │ │ └── TranslateEvent.java │ ├── factory/ │ │ └── TranslateServiceFactory.java │ ├── handler/ │ │ ├── CallBackHandler.java │ │ ├── EditToolEventHandler.java │ │ ├── FileOpenEventHandler.java │ │ ├── ToastEventHandler.java │ │ └── ToastHandler.java │ ├── mapper/ │ │ ├── InterfaceMapper.java │ │ ├── LanguageMapper.java │ │ ├── SearchCasesMapper.java │ │ └── VersionMapper.java │ ├── model/ │ │ ├── DTO/ │ │ │ ├── AvailableServiceInfo.java │ │ │ ├── Subtitle.java │ │ │ ├── TranslateResult.java │ │ │ └── Video.java │ │ ├── PO/ │ │ │ ├── FileRecord.java │ │ │ ├── Interface.java │ │ │ ├── Language.java │ │ │ ├── SearchCases.java │ │ │ ├── ServiceInterface.java │ │ │ └── Version.java │ │ └── search/ │ │ ├── Cases.java │ │ ├── Engine.java │ │ ├── Result.java │ │ └── Selector.java │ ├── service/ │ │ ├── ConfigService.java │ │ ├── Impl/ │ │ │ └── InterfaceServiceImpl.java │ │ ├── InterfaceService.java │ │ ├── SearchService.java │ │ └── translate/ │ │ ├── AliTranslateService.java │ │ ├── BaiduTranslateService.java │ │ ├── HuoShanTranslateService.java │ │ ├── TencentTranslateService.java │ │ ├── TranslateService.java │ │ └── thread/ │ │ ├── AliTranslateThread.java │ │ ├── BaiduTranslateThread.java │ │ ├── HuoShanTranslateThread.java │ │ ├── TencentTranslateThread.java │ │ └── TranslateThread.java │ └── utils/ │ ├── ArchiveUtil.java │ ├── CacheUtil.java │ ├── FileUtils.java │ ├── SubtitleUtil.java │ ├── TranslateUtil.java │ ├── search/ │ │ ├── HTMLParsing.java │ │ ├── JSONParsing.java │ │ ├── Parsing.java │ │ └── ParsingFactory.java │ └── submerge/ │ ├── SubmergeAPI.java │ ├── TimedLinesAPI.java │ ├── constant/ │ │ └── FontName.java │ ├── parser/ │ │ ├── ASSParser.java │ │ ├── BaseParser.java │ │ ├── LRCParser.java │ │ ├── ParserFactory.java │ │ ├── SRTParser.java │ │ ├── SubtitleParser.java │ │ └── exception/ │ │ ├── InvalidAssSubException.java │ │ ├── InvalidColorCode.java │ │ ├── InvalidFileException.java │ │ ├── InvalidSRTSubException.java │ │ └── InvalidSubException.java │ ├── subtitle/ │ │ ├── ass/ │ │ │ ├── ASSSub.java │ │ │ ├── ASSTime.java │ │ │ ├── Events.java │ │ │ ├── ScriptInfo.java │ │ │ └── V4Style.java │ │ ├── common/ │ │ │ ├── SubtitleLine.java │ │ │ ├── SubtitleTime.java │ │ │ ├── TimedLine.java │ │ │ ├── TimedObject.java │ │ │ └── TimedTextFile.java │ │ ├── config/ │ │ │ ├── Font.java │ │ │ └── SimpleSubConfig.java │ │ ├── lrc/ │ │ │ ├── LRCLine.java │ │ │ ├── LRCSub.java │ │ │ └── LRCTime.java │ │ └── srt/ │ │ ├── SRTLine.java │ │ ├── SRTSub.java │ │ └── SRTTime.java │ └── utils/ │ ├── ColorUtils.java │ ├── ConvertUtils.java │ └── EncodeUtils.java └── resources/ ├── application.yml ├── banner.txt ├── css/ │ ├── edit-tool.css │ ├── font.css │ ├── main-editor.css │ ├── quick-start.css │ ├── setting.css │ ├── speech-conversion.css │ ├── styles.css │ ├── subtitle-search.css │ ├── title-bar.css │ ├── toast.css │ └── tool-box.css ├── font/ │ └── buttersans-Rounded.otf ├── fxml/ │ ├── edit-tool.fxml │ ├── export.fxml │ ├── main-editor.fxml │ ├── main-view.fxml │ ├── quick-start.fxml │ ├── setting.fxml │ ├── sidebar-after.fxml │ ├── sidebar-before.fxml │ ├── sidebar-bottom.fxml │ ├── speech-conversion.fxml │ ├── subtitle-search.fxml │ ├── sync-editor.fxml │ ├── title-bar.fxml │ ├── toast.fxml │ ├── tool-box.fxml │ └── voice-convert.fxml ├── logback/ │ └── logback-spring.xml └── mapper/ └── InterfaceMapper.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/subtitles-view.yml ================================================ # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven name: CI Build on: push: paths-ignore: - 'README.md' branches: [ main ] pull_request: paths-ignore: - 'README.md' branches: [ main ] workflow_dispatch: inputs: generateInstaller: description: 'generateInstaller' required: true type: choice options: - 'true' - 'false' default: 'false' customizedJre: description: 'customizedJre' required: true type: choice options: - 'true' - 'false' default: 'false' jobs: bundling-for-windows: runs-on: windows-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn --file pom.xml -Dplatform=windows -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=true -DcreateTarball=false -B package - name: Archive production artifacts uses: actions/upload-artifact@v3 with: name: windows path: | target/subtitles-view-*.* !target/subtitles-view-*.jar bundling-for-linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn --file pom.xml -Dplatform=linux -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=false -DcreateTarball=true -B package - name: Archive production artifacts uses: actions/upload-artifact@v3 with: name: linux path: | target/subtitles-view-*.* !target/subtitles-view-*.jar bundling-for-mac: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn --file pom.xml -Dplatform=mac -DgenerateInstaller=${{github.event.inputs.bundleJre}} -DcustomizedJre=${{github.event.inputs.customizedJre}} -DcreateZipball=false -DcreateTarball=true -B package - name: Archive production artifacts uses: actions/upload-artifact@v3 with: name: mac path: | target/subtitles-view-*.* !target/subtitles-view-*.jar ================================================ FILE: .gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/** !**/src/test/** ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ ### VS Code ### .vscode/ /src/test ### custom ### /logs /src/test/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 fordes123 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Subtitles-View [![stars](https://img.shields.io/github/stars/fordes123/Subtitles-View?color=%23e74c3c)]() [![forks](https://img.shields.io/github/forks/fordes123/Subtitles-View?color=%232ecc71)]() [![release](https://img.shields.io/github/v/release/fordes123/Subtitles-View.svg)](https://github.com/fordes123/Subtitles-View/releases) [![license](https://img.shields.io/github/license/fordes123/Subtitles-View?color=%239b59b6)](https://opensource.org/licenses/MIT)   这是一个基于`JavaFX`的程序,致力于简单、优雅、高效处理和编辑字幕。适配SRT、ASS等字幕格式,并且支持视频语音转换与字幕翻译,欢迎体验. > ⚠️ 很遗憾,此仓库已停止维护 ## ✨ 特性 - 🎁 现代化的界面,简洁明快 - 🦄 在线语音转换,简单为视频生成字幕并翻译 - ☑️ 多种视频与字幕格式支持 - ✏ 便捷化字幕编辑功能,帮助快速修正机器翻译 - 🎯 在线的字幕搜索与下载 - 🎈 深色浅色模式一键切换 - ⛏ 更多特性待开发... ## 🎉 应用界面 ![浅色模式](./screenshot/home.png "⚠️界面可能已经更新,请以具体程序为准") ## ☑️ TODO - [x] 框架搭建以及迁移重构 - [x] UI调整,深浅色跟随系统等 - [x] 字幕搜索、下载支持:`字幕库`、`伪射手网`、`A4k字幕网` - [x] 文字翻译服务适配:`百度翻译`、`阿里翻译`、`腾讯翻译`、`火山翻译` - [ ] 语音转换服务适配 - [ ] 简单的视频处理支持,如字幕分离、水印、格式转换等 ## 🧑🏻‍🔧技术栈 - `Maven` - `JavaFX` - `SpringBoot` - `SQLite` - `Mybatis-Plus` ## 📢 项目说明 - 兴趣之作,欢迎提出任何修改意见,但不保证任何更新以及功能的可靠性 - 设计支持跨平台,但未经测试,现阶段以`Windows`平台为主 - 程序无任何收费和用户信息收集行为。所有在线服务如:语音转写、在线翻译均为第三方提供,与本程序无关 ## 🛠 快速开始 ### 从源代码构建 ```shell # 请保证你的JDK版本不低于11,否则无法通过编译 git clone https://github.com/fordes123/subtitles-view.git cd subtitles-view mvn clean install mvn run ``` 或者 fork 本项目, 在 `WorkFlows` 中运行 `CI Build`, Github Action 将根据配置自动为你构建对应程序包
查看引导
### 获取可执行文件 - 正式发行版 [🚀 Releases](https://github.com/fordes123/Subtitles-View/releases/) - 自动构建的测试版 [🤖 CI](https://github.com/fordes123/subtitles-view/actions) (由于正在积极开发中,暂时没有 Release 版本,预览以及体验可使用 CI 版本) ## 🤝 交流反馈 - 提交 [📌Issues](https://github.com/fordes123/Subtitles-View/issues) - 博客评论区 [📌Blog Page](https://blog.fordes.top/archives/subtitles-view.html) ## 📜 开源许可 - 基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。 ================================================ FILE: pom.xml ================================================ 4.0.0 subtitles-view org.fordes subtitles-view 2.0.0-Alpha subtitles-view UTF-8 UTF-8 2.6.2 1.3.0 1.18.22 5.7.18 11.0.2 9.0.9 3.8 1.14.3 3.36.0.3 3.5.1 16.02-2.01 1.6.7 2.4.0 0.10.9 windows true false false true false jitpack.io https://jitpack.io com.github.fordes123 spring-boot-jfx 0.0.1 org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all ${hutool.version} org.openjfx javafx-controls ${javafx.version} org.openjfx javafx-fxml ${javafx.version} org.openjfx javafx-base ${javafx.version} org.openjfx javafx-graphics ${javafx.version} com.jfoenix jfoenix ${jfoenix.version} org.jsoup jsoup ${jsoup.version} org.xerial sqlite-jdbc ${sqlite.version} com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} net.sf.sevenzipjbinding sevenzipjbinding ${sevenzipjbinding.version} net.sf.sevenzipjbinding sevenzipjbinding-all-platforms ${sevenzipjbinding.version} com.github.albfernandez juniversalchardet ${juniversalchardet.version} org.springframework.boot spring-boot-starter-json org.fxmisc.richtext richtextfx ${richtextfx.version} org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.maven.plugins maven-compiler-plugin 3.8.1 11 11 UTF-8 io.github.fvarrui javapackager ${javapackager.version} package package package ${platform} org.fordes.subtitles.view.SubtitlesViewApplication ${bundleJre} ${customizedJre} ${generateInstaller} false ${createZipball} ${createTarball} -XX:TieredStopAtLevel=1 -noverify ================================================ FILE: src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java ================================================ package org.fordes.subtitles.view; import cn.hutool.core.lang.Singleton; import cn.hutool.core.util.StrUtil; import com.jthemedetecor.OsThemeDetector; import javafx.scene.Parent; import javafx.stage.Stage; import javafx.stage.StageStyle; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.fordes.jfx.annotation.JFXApplication; import org.fordes.jfx.annotation.Tray; import org.fordes.jfx.core.ProxyApplication; import org.fordes.jfx.core.ProxyLauncher; import org.fordes.jfx.core.StageReadyEvent; import org.fordes.subtitles.view.config.ApplicationConfig; import org.fordes.subtitles.view.constant.StyleClassConstant; import org.fordes.subtitles.view.event.ThemeChangeEvent; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.awt.*; import java.io.IOException; /** * @author fordes */ @Slf4j @AllArgsConstructor @SpringBootApplication @JFXApplication(value = "/fxml/main-view.fxml", title = "SubtitlesView Alpha", style = StageStyle.TRANSPARENT, css = {"/css/styles.css", "/css/font.css"}, osThemeDetector = true, darkStyleClass = "dark", icons = {"/icon/logo.ico"}, systemTray = @Tray(value = true, image = "/icon/logo.png", toolTip = "SubtitlesView")) public class SubtitlesViewApplication extends ProxyApplication { private final ApplicationConfig config; public static String applicationName; private static final long timeMillis = System.currentTimeMillis(); @Value("${spring.application.name}") public void setApplicationName(String applicationName) { SubtitlesViewApplication.applicationName = applicationName; } public static void main(String[] args) { ProxyLauncher.run(SubtitlesViewApplication.class, args); } @Override public void handleEvent(StageReadyEvent event) throws IOException, AWTException { super.handleEvent(event); log.info("{} 启动成功! 耗时: {} ms", applicationName, System.currentTimeMillis() - timeMillis); } @Override public void loadFXMLBefore(Stage stage, JFXApplication property) { //stage存入单例池 Singleton.put(stage); super.loadFXMLBefore(stage, property); } @Override public void initAfter(Stage stage) { stage.getScene().setFill(null); //监听全屏状态,切换样式 stage.fullScreenProperty().addListener((observableValue, aBoolean, t1) -> { stage.getScene().getRoot().getStyleClass().remove(t1 ? StyleClassConstant.NORMAL_SCREEN : StyleClassConstant.FULL_SCREEN); stage.getScene().getRoot().getStyleClass().add(t1 ? StyleClassConstant.FULL_SCREEN : StyleClassConstant.NORMAL_SCREEN); }); super.initAfter(stage); } @Override public void registerOsThemeDetector(OsThemeDetector detector, Stage stage, JFXApplication property) { Parent root = stage.getScene().getRoot(); if (StrUtil.isNotEmpty(property.darkStyleClass())) { detector.registerListener(isDark -> { if (config.getTheme() == null) { switchTheme(detector, root, property, isDark); } }); //监听主题切换事件 stage.addEventHandler(ThemeChangeEvent.EVENT_TYPE, event -> switchTheme(detector, root, property, event.isDark())); //初始主题 switchTheme(detector, root, property, config.getTheme()); } } private void switchTheme(OsThemeDetector detector, Parent root, JFXApplication property, Boolean isDark) { if (isDark != null) { if (isDark) { if (!root.getStyleClass().contains(property.darkStyleClass())) { root.getStyleClass().add(property.darkStyleClass()); } } else { root.getStyleClass().remove(property.darkStyleClass()); } config.setCurrentTheme(isDark); } else { switchTheme(detector, root, property, detector.isDark()); } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java ================================================ package org.fordes.subtitles.view.config; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.resource.ClassPathResource; import cn.hutool.core.lang.Dict; import cn.hutool.json.JSONUtil; import cn.hutool.setting.yaml.YamlUtil; import com.baomidou.mybatisplus.annotation.TableField; import javafx.scene.text.Font; import lombok.Data; import org.fordes.subtitles.view.constant.CommonConstant; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.io.Serializable; import java.nio.charset.Charset; /** * @author fordes on 2022/4/17 */ @Data @Component @ConfigurationProperties(prefix = "config") public class ApplicationConfig implements Serializable { /** * 主题模式 false-浅色、true-深色、null-跟随系统 */ private Boolean theme = null; /** * 字体 */ private String fontFace = Font.getDefault().getFamily();; /** * 字体大小 */ private Integer fontSize = 18; /** * 编辑模式 false-简洁模式、true-完整模式 */ private Boolean editMode = Boolean.FALSE; /** * 退出模式 false-直接退出、true-最小化至托盘 */ private Boolean exitMode = Boolean.FALSE; /** * 默认文件输出路径 */ private String outPath = CommonConstant.PATH_HOME; /** * 语言列表选项 false-完整、true-精简 */ private Boolean languageListMode = Boolean.TRUE; @TableField(exist = false) private boolean currentTheme; private static final long serialVersionUID = 1L; private static final ClassPathResource resource = new ClassPathResource("application.yml"); /** * 写入配置文件 */ public void dump() { Dict all = YamlUtil.load(resource.getReader(Charset.defaultCharset())); all.put("config", JSONUtil.parseObj(this)); YamlUtil.dump(all, FileUtil.getWriter(resource.getFile(), Charset.defaultCharset(), false)); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java ================================================ package org.fordes.subtitles.view.config; import cn.hutool.core.thread.ExecutorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.ThreadPoolExecutor; /** * @author fordes on 2022/7/11 */ @Configuration public class ExecutorConfig { private final static int core = Runtime.getRuntime().availableProcessors(); @Bean("globalExecutor") public ThreadPoolExecutor globalExecutor() { return ExecutorBuilder.create() .setCorePoolSize(2 * core) .setMaxPoolSize(2 * core) .setHandler(new ThreadPoolExecutor.CallerRunsPolicy()) .build(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java ================================================ package org.fordes.subtitles.view.constant; /** * @author fordes on 2022/1/24 */ public class CommonConstant { public static final double SCENE_MIN_WIDTH = 1050.0; public static final double SCENE_MIN_HEIGHT = 700.0; public static final double SIDE_BAR_WIDTH = 250.0; public static final String PREFIX = "*."; public static final String TITLE_ALL_FILE = "选择文件以开始"; public static final String TITLE_PATH = "选择文件路径"; public static final String PATH_HOME = System.getProperty("user.home"); public static final String ROOT_PATH = System.getProperty("user.dir"); public static final String TEMP_PATH = ROOT_PATH+ "\\temp\\"; public static final String DOWNLOAD_PATH = TEMP_PATH+ "download\\"; // public static final String FILE_PATH = TEMP_PATH+ "file\\"; // public static final String LIB_PATH = ROOT_PATH+ "\\lib\\"; // // public static final String SEVEN_ZIP_PATH = LIB_PATH+ "7z"; /** * 7z解压命令 递归子目录、全部解压到指定文件夹、只解压指定格式文件 */ // public static final String UN_ARCHIVE_COMMAND_FORMAT = SEVEN_ZIP_PATH+ " e -aoa -bse0 -r {} -o{} {} -y"; public static final String CONCISE_MODE = "简洁模式"; public static final String FULL_MODE = "完整模式"; public static final String TRANSLATE_REPLACE = "替换模式"; public static final String TRANSLATE_BILINGUAL = "双语模式"; public static final String URL_HOME = "https://github.com/fordes123/subtitles-view"; public static final String URL_ISSUES = URL_HOME + "/issues"; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java ================================================ package org.fordes.subtitles.view.constant; /** * @author fordes on 2022/1/23 */ public class StyleClassConstant { public static final String NORMAL_SCREEN = "normal-screen"; public static final String FULL_SCREEN = "full-screen"; public static final String SUBTITLE_SEARCH_ENGINE_ITEM = "item"; public static final String SUBTITLE_SEARCH_ENGINE = "engine"; public static final String QUICK_START_FILE_CHOOSE_WARNING = "warning"; public static final String QUICK_START_FILE_CHOOSE_ERROR = "error"; public static final String QUICK_START_FILE_CHOOSE_SUCCESS = "success"; public static final String SUBTITLE_SEARCH_ITEM = "search-item"; public static final String SUBTITLE_SEARCH_ITEM_CAPTION = "caption"; public static final String SUBTITLE_SEARCH_ITEM_TEXT = "text"; public static final String CONTENT_EXCLUSIVE = "content-exclusive"; public static final String FONT_STYLE_TEMPLATE = "-fx-font-size: {};-fx-font-family: {}"; public static final String ERROR = "error"; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java ================================================ package org.fordes.subtitles.view.controller; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.Scene; import javafx.scene.layout.Pane; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.net.URL; import java.util.ResourceBundle; import java.util.concurrent.ThreadPoolExecutor; /** * 控制器抽象,继承并实现delayInit()方法即可在面板首次显示时进行初始化操作 * * @author fordes on 2022/4/22 */ @Component public abstract class DelayInitController implements Initializable { @FXML public Pane root; @Resource public ThreadPoolExecutor globalExecutor; private boolean isInit = false; @Override public void initialize(URL url, ResourceBundle resourceBundle) { root.visibleProperty().addListener((observableValue, aBoolean, t1) -> { if (!isInit && t1) { delay(); isInit = true; } }); globalExecutor.execute(this::async); } public Scene getScene() { return root.getScene(); } /** * 懒加载,在面板首次显示时执行 */ public void delay() {}; /** * 异步方法,在线程池中执行,避免主线程阻塞 */ public void async() {}; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/EditTool.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.Singleton; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.jfoenix.controls.JFXComboBox; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Parent; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ChoiceBox; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.layout.GridPane; import javafx.scene.text.Font; import javafx.stage.Stage; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.config.ApplicationConfig; import org.fordes.subtitles.view.constant.CommonConstant; import org.fordes.subtitles.view.constant.StyleClassConstant; import org.fordes.subtitles.view.enums.EditToolEventEnum; import org.fordes.subtitles.view.enums.FileEnum; import org.fordes.subtitles.view.enums.ServiceType; import org.fordes.subtitles.view.event.EditToolEvent; import org.fordes.subtitles.view.event.LoadingEvent; import org.fordes.subtitles.view.event.ToastChooseEvent; import org.fordes.subtitles.view.event.ToastConfirmEvent; import org.fordes.subtitles.view.factory.TranslateServiceFactory; import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo; import org.fordes.subtitles.view.model.DTO.Subtitle; import org.fordes.subtitles.view.model.PO.Language; import org.fordes.subtitles.view.service.InterfaceService; import org.fordes.subtitles.view.service.translate.TranslateService; import org.fordes.subtitles.view.utils.CacheUtil; import org.fordes.subtitles.view.utils.SubtitleUtil; import org.fordes.subtitles.view.utils.submerge.subtitle.ass.ASSTime; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime; import org.fxmisc.richtext.StyleClassedTextArea; import org.fxmisc.richtext.model.TwoDimensional; import org.mozilla.universalchardet.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * 编辑工具 控制器 * * @author fordes on 2022/7/15 */ @Slf4j @Component public class EditTool extends DelayInitController { private static Subtitle subtitle; private static StyleClassedTextArea area; private static ToggleButton editMode; private static int max; private static final Map bindMap = MapUtil.newHashMap(); @FXML private CheckMenuItem search_case, search_regex, replace_case, replace_regex; @FXML private JFXComboBox code_choice, font_family; @FXML private ChoiceBox translate_source; @FXML private ChoiceBox translate_mode; @FXML private JFXComboBox translate_original, translate_target; @FXML private JFXComboBox font_size; @FXML private ChoiceBox timeline_option; @FXML private TextField timeline_input, jump_input, search_input, replace_input, replace_find_input; private final InterfaceService interfaceService; private final SidebarBottom sidebarBottom; private final ApplicationConfig config; @Autowired public EditTool(InterfaceService interfaceService, SidebarBottom sidebarBottom, ApplicationConfig config) { this.interfaceService = interfaceService; this.sidebarBottom = sidebarBottom; this.config = config; } @Override public void delay() { //编码选择框 code_choice.getItems().addAll(Arrays.stream(ReflectUtil.getFieldsValue(Constants.class)) .map(Object::toString).toArray(String[]::new)); //初始化字体大小 font_size.getItems().addAll(CollUtil.newArrayList(12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72)); //初始化字体列表 font_family.getItems().addAll(Font.getFontNames()); //时间轴校正选项 timeline_option.getItems().addAll(TimelineType.values()); timeline_option.getSelectionModel().selectedItemProperty().addListener((observableValue, strings, t1) -> timeline_input.setPromptText(t1.desc)); timeline_option.getSelectionModel().select(0); timeline_input.textProperty().addListener((observableValue, s, t1) -> timeline_input.getStyleClass().remove("error")); //翻译相关 translate_original.getSelectionModel().selectedItemProperty().addListener((observableValue, strings, t1) -> { if (t1 != null) { Collection gap = CollUtil.subtract(config.getLanguageListMode() ? t1.getTarget().stream().filter(e -> e.isGeneral() == config.getLanguageListMode()).collect(Collectors.toList()) : t1.getTarget(), translate_target.getItems()); Collection neg = CollUtil.subtract(translate_target.getItems(), t1.getTarget()); if (!gap.isEmpty()) { translate_target.getItems().addAll(gap); } if (!neg.isEmpty()) { translate_target.getItems().removeAll(neg); } } else translate_target.getItems().clear(); }); translate_source.getSelectionModel().selectedItemProperty() .addListener((observableValue, availableServiceInfo, t1) -> { if (t1 != null) { translate_original.getItems().clear(); translate_original.getItems().addAll(CacheUtil.getLanguageDict(ServiceType.TRANSLATE, t1.getProvider(), config.getLanguageListMode())); translate_original.getSelectionModel().selectFirst(); } }); translate_source.getItems().clear(); translate_source.getItems().addAll(interfaceService.getAvailableService(ServiceType.TRANSLATE)); translate_source.getSelectionModel().selectFirst(); translate_mode.getItems().addAll(CommonConstant.TRANSLATE_REPLACE, CommonConstant.TRANSLATE_BILINGUAL); translate_mode.getSelectionModel().selectFirst(); //回车提交操作 timeline_input.setOnAction(this::applyTimeline); jump_input.setOnAction(this::applyJump); search_input.setOnAction(this::applySearch); replace_find_input.setOnAction(this::applyReplaceFind); replace_input.setOnAction(this::applyReplaceNext); //错误输入 jump_input.textProperty().addListener((observableValue, s, t1) -> jump_input.getStyleClass().remove(StyleClassConstant.ERROR)); search_input.textProperty().addListener((observableValue, s, t1) -> search_input.getStyleClass().remove(StyleClassConstant.ERROR)); timeline_input.textProperty().addListener((observableValue, s, t1) -> timeline_input.getStyleClass().remove(StyleClassConstant.ERROR)); } @Override public void async() { Stage stage = Singleton.get(Stage.class); //各工具面板互斥 root.getChildren().forEach(node -> { if (node instanceof GridPane) { EditToolEventEnum type = EditToolEventEnum.valueOf((String) node.getUserData()); bindMap.put(type, (GridPane) node); node.visibleProperty().addListener((observableValue, aBoolean, t1) -> { if (t1) { bindMap.values().forEach(e -> e.setVisible(node.equals(e))); root.setVisible(true); } }); } } ); root.visibleProperty().addListener((observableValue, aBoolean, t1) -> { if (!t1) { bindMap.values().forEach(e -> e.setVisible(false)); } }); //监听编辑工具事件 唤起对应功能面板 stage.addEventHandler(EditToolEvent.EVENT_TYPE, event -> { subtitle = event.getSubtitle(); area = event.getSource(); editMode = event.getEditMode(); Parent parent = bindMap.get(event.getType()); switch (event.getType()) { case SEARCH: //搜索 search_input.requestFocus(); parent.setVisible(true); break; case REPLACE://替换 replace_find_input.requestFocus(); parent.setVisible(true); break; case JUMP://跳转 jump_input.requestFocus(); max = 0; for (TimedLine timedLine : subtitle.getTimedTextFile().getTimedLines()) { max += timedLine.getTextLines().size(); } parent.setVisible(true); break; case FONT: //字体(样式) // font_family.getSelectionModel().select(config.getFontFace()); font_size.getSelectionModel().select(config.getFontSize()); parent.setVisible(true); break; case TIMELINE: //时间轴 TimedLine start = CollUtil.getFirst(subtitle.getTimedTextFile().getTimedLines()); timeline_input.setPromptText(start.getTime().getStart().toString()); timeline_input.requestFocus(); parent.setVisible(true); break; case CODE://编码 code_choice.getSelectionModel().select(subtitle.getCharset()); parent.setVisible(true); break; case TRANSLATE: List list = interfaceService.getAvailableService(ServiceType.TRANSLATE); if (list.isEmpty()) { stage.fireEvent(new ToastChooseEvent("未配置翻译服务", "是否立即转到设置?", "确定", () -> sidebarBottom.getSetting().getOnAction().handle(null))); parent.setVisible(false); } else { Collection gap = CollUtil.subtract(list, translate_source.getItems()); Collection neg = CollUtil.subtract(translate_source.getItems(), list); if (!gap.isEmpty()) { translate_source.getItems().addAll(gap); } if (!neg.isEmpty()) { translate_source.getItems().removeAll(neg); } parent.setVisible(true); } break; case REF: //刷新 try { SubtitleUtil.parse(subtitle); area.clear(); area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), "styled-text-area"); area.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE,config.getFontSize(), config.getFontFace())); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); stage.fireEvent(new ToastConfirmEvent("编码更改出错", "已切换回原编码~")); } break; } }); } @FXML private void onClose(ActionEvent actionEvent) { actionEvent.consume(); area = null; subtitle = null; editMode = null; root.setVisible(false); } @FXML private void applyCode(ActionEvent actionEvent) { String original = subtitle.getCharset(); try { subtitle.setCharset(code_choice.getSelectionModel().getSelectedItem()); SubtitleUtil.parse(subtitle); area.clear(); area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), StrUtil.EMPTY); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("编码更改出错", "已切换回原编码~")); subtitle.setCharset(original); code_choice.getSelectionModel().select(original); } actionEvent.consume(); } @FXML private void applyFont(ActionEvent actionEvent) { Stage stage = Singleton.get(Stage.class); String originalFontFamily = config.getFontFace(); Integer originalFontSize = config.getFontSize(); try { config.setFontSize(Convert.toInt(font_size.getValue())); config.setFontFace(font_family.getValue()); area.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE, config.getFontSize(), config.getFontFace())); area.requestFocus(); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); config.setFontSize(originalFontSize); config.setFontFace(originalFontFamily); font_family.setValue(originalFontFamily); font_size.setValue(originalFontSize); stage.fireEvent(new ToastConfirmEvent("字体更改出错", "已切换回原字体~")); } actionEvent.consume(); } @FXML private void applyTimeline(ActionEvent actionEvent) { LocalTime newTime = null; String timeLine = timeline_input.getText(); TimelineType option = timeline_option.getValue(); if (TimelineType.TIMELINE.equals(option)) { try { newTime = FileEnum.SRT.equals(subtitle.getFormat()) ? SRTTime.fromString(timeLine) : ASSTime.fromString(timeLine); } catch (Exception ignored) { } } else { if (NumberUtil.isInteger(timeLine)) { int offset = Convert.toInt(timeLine); LocalTime date = CollUtil.getFirst(subtitle.getTimedTextFile().getTimedLines()).getTime().getStart(); newTime = date.plus(offset, option.rate); } } if (newTime != null) { //TODO 按选中范围处理 待支持 TimedTextFile original = subtitle.getTimedTextFile(); try { TimedTextFile target = SubtitleUtil .revise(subtitle.getTimedTextFile(), newTime, null, editMode.isSelected()); subtitle.setTimedTextFile(target); SubtitleUtil.write(subtitle, success -> { Singleton.get(Stage.class).fireEvent(new LoadingEvent(!success)); if (success) { area.clear(); area.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), StrUtil.EMPTY); } else throw new RuntimeException("写入失败"); }); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); subtitle.setTimedTextFile(original); Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("时间轴更改出错", "已切换回原时间轴~")); } } else timeline_input.getStyleClass().add(StyleClassConstant.ERROR); actionEvent.consume(); } @FXML private void applyJump(ActionEvent actionEvent) { String text = jump_input.getText(); int value = NumberUtil.isInteger(text) ? NumberUtil.parseInt(text) : 0; if (value > 0 && value <= max) { TwoDimensional.Position position = area.position(value, 1); area.moveTo(position.toOffset()); area.requestFollowCaret(); } else { jump_input.getStyleClass().add(StyleClassConstant.ERROR); } actionEvent.consume(); } @FXML private void applySearch(ActionEvent actionEvent) { String str = search_input.getText(); if (StrUtil.isNotBlank(str)) { SubtitleUtil.search(area, str, search_case.isSelected(), search_regex.isSelected()); } else search_input.getStyleClass().add(StyleClassConstant.ERROR); actionEvent.consume(); } @FXML private void applyReplaceNext(ActionEvent actionEvent) { applyReplace(false); actionEvent.consume(); } @FXML private void applyReplaceAll(ActionEvent actionEvent) { applyReplace(true); actionEvent.consume(); } @FXML private void applyReplaceFind(ActionEvent actionEvent) { String str = replace_find_input.getText(); if (StrUtil.isNotBlank(str)) { SubtitleUtil.find(area, str, replace_case.isSelected(), replace_regex.isSelected()); } actionEvent.consume(); } private void applyReplace(boolean isAll) { if (editMode.isSelected()) { String replaceText = replace_input.getText(); String searchText = replace_find_input.getText(); if (StrUtil.isAllNotBlank(replaceText, searchText)) { try { SubtitleUtil.replace(area, subtitle, searchText, replaceText, isAll, replace_case.isSelected(), replace_regex.isSelected()); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("替换出错", "已切换回原文本~")); } } } else Singleton.get(Stage.class).fireEvent(new ToastChooseEvent("操作受限", "是否切换至完整模式?", "切换", () -> editMode.setSelected(true))); } @FXML private void applyTranslate(ActionEvent actionEvent) { AvailableServiceInfo source = translate_source.getValue(); boolean mode = StrUtil.equals(CommonConstant.TRANSLATE_BILINGUAL, translate_mode.getValue()); Language origin = translate_original.getValue(); Language target = translate_target.getValue(); if (source != null && origin != null && target != null) { TranslateService service = TranslateServiceFactory.getService(source.getProvider().getValue()); Singleton.get(Stage.class).fireEvent(new LoadingEvent(true)); globalExecutor.execute(() -> service.translate(subtitle, target.getCode(), origin.getCode(), source.getVersionInfo(), mode, JSONUtil.parseObj(source.getAuth()))); } actionEvent.consume(); } /** * 时间轴校正 操作类型枚举 */ @AllArgsConstructor enum TimelineType { TIMELINE("时间轴", null, "形如: xx:xx:xx:xx"), SECOND("秒", ChronoUnit.SECONDS, "整数,时间偏移量"), MILLISECOND("毫秒", ChronoUnit.MILLIS, "整数,时间偏移量"), MINUTE("分钟", ChronoUnit.MINUTES, "整数,时间偏移量"), HOUR("小时", ChronoUnit.HOURS, "整数,的时间偏移量"); public final String name; public final ChronoUnit rate; public final String desc; @Override public String toString() { return this.name; } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/Export.java ================================================ package org.fordes.subtitles.view.controller; import org.springframework.stereotype.Component; /** * 语音转换 控制器 * * @author fordes on 2022/4/8 */ @Component public class Export { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/MainController.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.lang.Singleton; import javafx.fxml.FXML; import javafx.scene.Cursor; import javafx.scene.Parent; import javafx.scene.control.Label; import javafx.scene.input.MouseEvent; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.constant.CommonConstant; import org.fordes.subtitles.view.constant.StyleClassConstant; import org.fordes.subtitles.view.enums.FontIcon; import org.fordes.subtitles.view.event.FileOpenEvent; import org.fordes.subtitles.view.event.LoadingEvent; import org.springframework.stereotype.Component; /** * @author fordes on 2022/1/19 */ @Slf4j @Component public class MainController extends DelayInitController { @FXML private StackPane loading; @FXML private ColumnConstraints sidebarColumn; @FXML private Label drawer; @FXML private SidebarBefore sidebarBeforeController; @FXML private SidebarAfter sidebarAfterController; @FXML private SidebarBottom sidebarBottomController; @FXML private GridPane content; @FXML private Parent quickStart, subtitleSearch, toolBox, setting, export, mainEditor, syncEditor, voiceConvert, sidebarBefore, sidebarAfter; private static double xOffset = 0; private static double yOffset = 0; private static int bit = 0; private final static double RESIZE_WIDTH = 5.00; @Override public void delay() { content.getChildren().forEach(node -> node.visibleProperty().addListener((observableValue, aBoolean, t1) -> { if (t1) { content.getChildren().forEach(e -> e.setVisible(e.equals(node))); } })); } @Override public void async() { //绑定侧边按键和对应面板显示 sidebarBeforeController.getQuickStart().setOnAction(event -> { sidebarBeforeController.getQuickStart().setSelected(true); quickStart.setVisible(true); }); sidebarBeforeController.getSubtitleSearch().setOnAction(event -> { sidebarBeforeController.getSubtitleSearch().setSelected(true); subtitleSearch.setVisible(true); }); sidebarBeforeController.getToolBox().setOnAction(event -> { sidebarBeforeController.getToolBox().setSelected(true); toolBox.setVisible(true); }); sidebarAfterController.getMainEditor().setOnAction(event -> { sidebarAfterController.getMainEditor().setSelected(true); mainEditor.setVisible(true); }); sidebarAfterController.getSyncEditor().setOnAction(event -> { sidebarAfterController.getSyncEditor().setSelected(true); syncEditor.setVisible(true); }); sidebarAfterController.getExport().setOnAction(event -> { sidebarAfterController.getExport().setSelected(true); export.setVisible(true); }); sidebarBottomController.getSetting().setOnAction(event -> { setting.setVisible(true); sidebarAfterController.getItemGroup().selectToggle(null); sidebarBeforeController.getItemGroup().selectToggle(null); }); content.getChildren().forEach(node -> node.visibleProperty().addListener((observableValue, aBoolean, t1) -> { if (t1) { content.getChildren().forEach(e -> e.setVisible(e.equals(node))); } })); Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> { if (fileOpenEvent.getRecord().getFormat().media) { sidebarAfterController.getItemGroup().selectToggle(null); sidebarBeforeController.getItemGroup().selectToggle(null); }else { sidebarBefore.setVisible(false); sidebarAfter.setVisible(true); } }); Singleton.get(Stage.class).addEventHandler(LoadingEvent.EVENT_TYPE, loadingEvent -> loading.setVisible(loadingEvent.isAlive())); } @FXML private void mousePressedHandle(MouseEvent event) { event.consume(); xOffset = event.getSceneX(); yOffset = event.getSceneY(); } @FXML private void mouseMoveHandle(MouseEvent event) { event.consume(); double x = event.getSceneX(); double y = event.getSceneY(); double width = Singleton.get(Stage.class).getWidth() - 20; double height = Singleton.get(Stage.class).getHeight() - 20; Cursor cursorType = Cursor.DEFAULT; bit = 0; if (y >= height - RESIZE_WIDTH) { if (x <= RESIZE_WIDTH) { bit |= 1 << 3; } else if (x >= width - RESIZE_WIDTH) { bit |= 1; bit |= 1 << 2; cursorType = Cursor.SE_RESIZE; } else { bit |= 1; cursorType = Cursor.S_RESIZE; } } else if (x >= width - RESIZE_WIDTH) { bit |= 1 << 2; cursorType = Cursor.E_RESIZE; } getScene().getRoot().setCursor(cursorType); } @FXML private void mouseDraggedHandle(MouseEvent event) { Stage stage = Singleton.get(Stage.class); event.consume(); double x = event.getSceneX(); double y = event.getSceneY(); double nextX = stage.getX(); double nextY = stage.getY(); double nextWidth = stage.getWidth(); double nextHeight = stage.getHeight(); if ((bit & 1 << 2) != 0) { nextWidth = x; } if ((bit & 1) != 0) { nextHeight = y; } if (nextWidth <= CommonConstant.SCENE_MIN_WIDTH) { nextWidth = CommonConstant.SCENE_MIN_WIDTH; } if (nextHeight <= CommonConstant.SCENE_MIN_HEIGHT) { nextHeight = CommonConstant.SCENE_MIN_HEIGHT; } stage.setX(nextX); stage.setY(nextY); stage.setWidth(nextWidth); stage.setHeight(nextHeight); } @FXML private void titleBarDraggedHandle(MouseEvent event) { Stage stage = Singleton.get(Stage.class); stage.setX(event.getScreenX() - xOffset); stage.setY(event.getScreenY() - yOffset); event.consume(); } @FXML private void onDrawer(MouseEvent event) { if (sidebarColumn.getPrefWidth() > 0) { sidebarColumn.setPrefWidth(0); drawer.setText(FontIcon.PLACE_THE_LEFT.toString()); content.getStyleClass().add(StyleClassConstant.CONTENT_EXCLUSIVE); } else { sidebarColumn.setPrefWidth(CommonConstant.SIDE_BAR_WIDTH); drawer.setText(FontIcon.PLACE_THE_RIGHT.toString()); content.getStyleClass().remove(StyleClassConstant.CONTENT_EXCLUSIVE); } event.consume(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/MainEditor.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.Singleton; import cn.hutool.core.util.StrUtil; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.ToggleButton; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.MouseEvent; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.RowConstraints; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.config.ApplicationConfig; import org.fordes.subtitles.view.constant.CommonConstant; import org.fordes.subtitles.view.constant.StyleClassConstant; import org.fordes.subtitles.view.enums.EditToolEventEnum; import org.fordes.subtitles.view.enums.FontIcon; import org.fordes.subtitles.view.event.*; import org.fordes.subtitles.view.model.DTO.Subtitle; import org.fordes.subtitles.view.utils.SubtitleUtil; import org.fxmisc.richtext.StyleClassedTextArea; import org.fxmisc.richtext.model.TwoDimensional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 语音转换 控制器 * * @author fordes on 2022/4/8 */ @Slf4j @Component public class MainEditor extends DelayInitController { @FXML private GridPane editTool; @FXML private Label indicator, editModeIcon; @FXML private ToggleButton editMode; @FXML private StyleClassedTextArea editor; @FXML private HBox toolbarPanel; @FXML private RowConstraints toolbarRow; private Subtitle subtitle; private final ApplicationConfig config; @Autowired public MainEditor(ApplicationConfig config) { this.config = config; } @Override public void delay() { Stage stage = Singleton.get(Stage.class); //工具栏按钮,点击按钮发送编辑工具事件 唤起编辑工具 toolbarPanel.getChildren().forEach(node -> { if (node.getUserData() != null) { node.setOnMouseClicked(event -> { if (node.getUserData() != null) { EditToolEventEnum type = EditToolEventEnum.valueOf((String) node.getUserData()); stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, type)); } }); } }); //编辑模式监听 editMode.selectedProperty().addListener((observableValue, aBoolean, t1) -> { ctrlEditMode(t1); editor.clear(); editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), t1), StrUtil.EMPTY); }); //行列号监听 editor.caretPositionProperty().addListener((observable, oldValue, newValue) -> { TwoDimensional.Position position = editor.offsetToPosition(newValue, TwoDimensional.Bias.Backward); indicator.setText(StrUtil.format((String) indicator.getUserData(), position.getMajor(), position.getMinor())); }); // stage.addEventHandler(ThemeChangeEvent.EVENT_TYPE, event -> { // editor.setStyleClass(0, editor.getLength(), config.isCurrentTheme()? "richtext_dark":"richtext_light"); // }); stage.addEventHandler(TranslateEvent.EVENT_TYPE, event -> { if (TranslateEvent.SUCCESS.equals(event.getMsg())) { editor.clear(); editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), "styled-text-area"); editor.moveTo(0); } Platform.runLater(() -> { stage.fireEvent(new ToastConfirmEvent(event.getMsg(), event.getDetail())); stage.fireEvent(new LoadingEvent(false)); }); }); //快捷键 KeyCodeCombination ctrlT = new KeyCodeCombination(KeyCode.T, KeyCodeCombination.CONTROL_DOWN); stage.getScene().getAccelerators().put(ctrlT, this::ctrlToolbar); KeyCodeCombination ctrlF = new KeyCodeCombination(KeyCode.F, KeyCodeCombination.CONTROL_DOWN); stage.getScene().getAccelerators().put(ctrlF, () -> stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.SEARCH))); KeyCodeCombination ctrlR = new KeyCodeCombination(KeyCode.R, KeyCodeCombination.CONTROL_DOWN); stage.getScene().getAccelerators().put(ctrlR, () -> stage.fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.REPLACE))); } @Override public void async() { Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> { if (fileOpenEvent.getRecord().getFormat().subtitle) { subtitle = (Subtitle) fileOpenEvent.getRecord(); log.debug("主编辑器 => {}", subtitle.getFile().getPath()); try { Singleton.get(Stage.class).fireEvent(new LoadingEvent(true)); SubtitleUtil.parse(subtitle); root.setVisible(true); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("读取失败!", "字幕文件已经损坏")); } finally { Singleton.get(Stage.class).fireEvent(new LoadingEvent(false)); } } }); //载入设置 root.visibleProperty().addListener((observableValue, aBoolean, t1) -> { if (t1) { editor.setStyle(StrUtil.format(StyleClassConstant.FONT_STYLE_TEMPLATE, config.getFontSize(), config.getFontFace())); editor.clear(); editor.append(SubtitleUtil.toStr(subtitle.getTimedTextFile(), editMode.isSelected()), "styled-text-area"); //编辑器模式 ctrlEditMode(config.getEditMode()); }else { editTool.setVisible(false); } }); } @FXML private void hideToolbar(ActionEvent actionEvent) { ctrlToolbar(false); actionEvent.consume(); } /** * 控制工具栏显示/隐藏 * * @param state 状态 */ private void ctrlToolbar(boolean state) { toolbarRow.setMaxHeight(state ? 60 : 0); toolbarRow.setMinHeight(state ? 60 : 0); toolbarRow.setPrefHeight(state ? 60 : 0); toolbarPanel.setVisible(state); } private void ctrlToolbar() { ctrlToolbar(!toolbarPanel.isVisible()); } private void ctrlEditMode(Boolean mode) { if (mode == null) { mode = config.getEditMode(); } else { config.setEditMode(mode); } editModeIcon.setText(mode ? FontIcon.SWITCH_ON_DARK.toString() : FontIcon.SWITCH_OFF_DARK.toString()); editMode.setText(mode ? CommonConstant.FULL_MODE : CommonConstant.CONCISE_MODE); editMode.setSelected(mode); } @FXML private void changeEditMode(ActionEvent actionEvent) { actionEvent.consume(); } @FXML private void onIndicatorClicked(MouseEvent mouseEvent) { Singleton.get(Stage.class).fireEvent(new EditToolEvent(editor, subtitle, editMode, EditToolEventEnum.JUMP)); mouseEvent.consume(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/QuickStart.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Singleton; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.GridPane; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.constant.CommonConstant; import org.fordes.subtitles.view.constant.StyleClassConstant; import org.fordes.subtitles.view.enums.FileEnum; import org.fordes.subtitles.view.event.FileOpenEvent; import org.fordes.subtitles.view.utils.FileUtils; import org.springframework.stereotype.Component; import java.io.File; /** * @author fordes on 2022/2/6 */ @Slf4j @Component public class QuickStart { @FXML private Label clues; @FXML private GridPane root; private static File dragFile; private static final String UNSUPPORTED_FILE_TYPE = "不支持的文件类型"; private static final String DRAG_SUPPORT = "松手以打开文件"; private static final String TIPS_DEFAULT = "拖放或选择文件以继续"; private static final String OPEN_FILE_ERROR = "打开文件出错"; @FXML private void chooseFile(ActionEvent event) { File file = FileUtils.chooseFile(CommonConstant.TITLE_ALL_FILE, FileEnum.values()) .showOpenDialog(Singleton.get(Stage.class)); //读取文件信息 if (FileUtil.exist(file) && FileEnum.isSupport(FileUtil.getSuffix(file))) { Singleton.get(Stage.class).fireEvent(new FileOpenEvent(dragFile)); } else { root.getStyleClass().clear(); clues.setText(TIPS_DEFAULT); } event.consume(); } @FXML private void onDragOver(DragEvent dragEvent) { Dragboard db = dragEvent.getDragboard(); if (db.hasFiles()) { dragFile = db.getFiles().get(0); if (FileUtil.exist(dragFile) && FileEnum.isSupport(FileUtil.getSuffix(dragFile))) { dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE); clues.setText(DRAG_SUPPORT); root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_SUCCESS); } else { clues.setText(UNSUPPORTED_FILE_TYPE); root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_WARNING); dragFile = null; } } dragEvent.consume(); } @FXML private void onDragExited(DragEvent dragEvent) { clues.setText(TIPS_DEFAULT); root.getStyleClass().clear(); dragEvent.consume(); } @FXML private void onDragDropped(DragEvent dragEvent) { try { if (dragFile != null) { Singleton.get(Stage.class).fireEvent(new FileOpenEvent(dragFile)); } } catch (Exception e) { clues.setText(OPEN_FILE_ERROR); root.getStyleClass().add(StyleClassConstant.QUICK_START_FILE_CHOOSE_ERROR); } finally { dragEvent.consume(); } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/Setting.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Singleton; import cn.hutool.core.swing.DesktopUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXComboBox; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.TextFlow; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.config.ApplicationConfig; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.enums.ServiceType; import org.fordes.subtitles.view.event.ThemeChangeEvent; import org.fordes.subtitles.view.event.ToastConfirmEvent; import org.fordes.subtitles.view.model.PO.ServiceInterface; import org.fordes.subtitles.view.model.PO.Version; import org.fordes.subtitles.view.service.InterfaceService; import org.fordes.subtitles.view.utils.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.File; /** * 语音转换 控制器 * * @author fordes on 2022/4/8 */ @Slf4j @Component public class Setting extends DelayInitController { @FXML private VBox infoPanel; @FXML private TextFlow tips; @FXML private ToggleGroup themeGroup, editorModeGroup, exitModeGroup, languageListGroup; @FXML private JFXComboBox version; @FXML private JFXComboBox type; @FXML private JFXComboBox provider; @FXML private JFXComboBox fontFace; @FXML private JFXComboBox fontSize; @FXML private TextField outPath; private final InterfaceService interfaceService; private final ApplicationConfig config; @Autowired public Setting(ApplicationConfig config, InterfaceService interfaceService) { this.config = config; this.interfaceService = interfaceService; } @Override public void delay() { Stage stage = Singleton.get(Stage.class); //初始化首选项 fontFace.getItems().addAll(Font.getFontNames()); fontSize.getItems().addAll(CollUtil.newArrayList(10, 12, 14, 16, 18, 20, 24, 36)); applyConfig(); //首选项监听事件 themeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) -> { Boolean value = Convert.toBool(t1.getUserData()); config.setTheme(value); stage.fireEvent(new ThemeChangeEvent(value)); }); editorModeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) -> config.setEditMode(Convert.toBool(t1.getUserData()))); exitModeGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) -> config.setExitMode(Convert.toBool(t1.getUserData()))); fontFace.getSelectionModel().selectedItemProperty().addListener((observableValue, s, t1) -> config.setFontFace(t1)); fontSize.getSelectionModel().selectedItemProperty().addListener((observableValue, s, t1) -> config.setFontSize(t1)); outPath.textProperty().addListener((observableValue, s, t1) -> config.setOutPath(StrUtil.trim(t1))); languageListGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) -> config.setLanguageListMode(Convert.toBool(t1.getUserData()))); //接口类型 type.getItems().addAll(ServiceType.values()); type.getSelectionModel().selectedItemProperty().addListener((observableValue, type, t1) -> { if (null != t1 && provider.getValue() != null) { version.getItems().clear(); version.getItems().addAll(interfaceService.getVersions(t1, provider.getValue())); } }); //服务商 provider.getItems().addAll(ServiceProvider.values()); provider.getSelectionModel().selectedItemProperty().addListener((observableValue, supportDto, t1) -> { if (null != t1 && type.getValue() != null) { version.getItems().clear(); version.getItems().addAll(interfaceService.getVersions(type.getValue(), t1)); } }); //版本 version.getSelectionModel().selectedItemProperty().addListener((observableValue, serviceVersion, t1) -> { if (null != t1) { tips.setVisible(false); version.setTooltip(new Tooltip(t1.getRemark())); buildInfoFrame(interfaceService.getInterface(type.getValue(), provider.getValue())); } else { tips.setVisible(true); } }); //提示区 tips.visibleProperty().addListener((observableValue, aBoolean, t1) -> infoPanel.setVisible(!t1)); } @Override public void async() { //监听器用于保存配置 root.visibleProperty().addListener((observableValue, aBoolean, t1) -> { if (!t1) { if (FileUtil.exist(config.getOutPath())) { config.setOutPath(outPath.getText().trim()); } else { outPath.setText(config.getOutPath()); } config.dump(); } else { //每次显示前重新初始化一次 applyConfig(); } }); } /** * 从配置文件应用设置项 */ void applyConfig() { //读取配置设置默认值 fontFace.getSelectionModel().select(config.getFontFace()); fontSize.getSelectionModel().select(config.getFontSize()); editorModeGroup.getToggles().forEach(item -> { if (Convert.toBool(item.getUserData()).equals(config.getEditMode())) { item.setSelected(true); } }); themeGroup.getToggles().forEach(item -> { if (ObjectUtil.equal(config.getTheme(), Convert.toBool(item.getUserData()))) { item.setSelected(true); } }); exitModeGroup.getToggles().forEach(item -> { if (Convert.toBool(item.getUserData()).equals(config.getExitMode())) { item.setSelected(true); } }); outPath.setText(config.getOutPath()); } void buildInfoFrame(ServiceInterface info) { infoPanel.getChildren().clear(); JSONUtil.parseObj(StrUtil.isBlank(info.getAuth()) ? info.getTemplate() : info.getAuth()) .forEach((k, v) -> { HBox hBox = new HBox(); hBox.setMinHeight(90); hBox.setAlignment(Pos.CENTER_LEFT); Label label = new Label(k); label.setMinSize(120, 90); label.getStyleClass().add("item"); HBox.setMargin(label, new Insets(0, 0, 0, 30)); hBox.getChildren().add(label); TextField textField = new TextField(ObjectUtil.isNotEmpty(v) ? v.toString() : StrUtil.EMPTY); textField.getStyleClass().add("item"); textField.setUserData(k); textField.setMinSize(140, 90); hBox.getChildren().add(textField); infoPanel.getChildren().add(hBox); }); JFXButton save = new JFXButton("保存"); save.setPrefSize(80, 30); save.getStyleClass().add("normal-button"); save.setUserData(info); save.setOnAction(event -> { JSONObject param = new JSONObject(); infoPanel.getChildren().forEach(e -> { if (e instanceof TextField) { param.putOpt((String) e.getUserData(), ((TextField) e).getText()); } }); ServiceInterface data = (ServiceInterface) save.getUserData(); data.setAuth(param.toString()); try { if (interfaceService.updateById(info)) { tips.setVisible(true); Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("保存成功", "接口信息已经保存")); return; } } catch (Exception e) { log.error("接口信息保存失败 => {}", JSONUtil.toJsonStr(info)); log.error(ExceptionUtil.stacktraceToString(e)); } Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("保存失败", "数据操作失败,错误已记录")); }); HBox hBox = new HBox(); hBox.setMinHeight(90); hBox.setAlignment(Pos.CENTER_RIGHT); HBox.setMargin(save, new Insets(0, 30, 0, 0)); hBox.getChildren().add(save); if (StrUtil.isNotEmpty(info.getPage())) { JFXButton applyFor = new JFXButton("去申请"); applyFor.setPrefSize(80, 30); applyFor.getStyleClass().add("normal-button"); applyFor.setTooltip(new Tooltip(info.getPage())); applyFor.setOnAction(event -> DesktopUtil.browse(info.getPage())); hBox.getChildren().add(applyFor); } infoPanel.getChildren().add(hBox); } @FXML private void onChooseOutPath(MouseEvent event) { File path = FileUtils.choosePath(outPath.getText().trim()).showDialog(Singleton.get(Stage.class)); if (path != null && StrUtil.isNotEmpty(path.getPath())) { outPath.setText(path.getPath()); } event.consume(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java ================================================ package org.fordes.subtitles.view.controller; import javafx.fxml.FXML; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import lombok.Getter; import org.springframework.stereotype.Component; /** * @author fordes on 2022/4/8 */ @Component public class SidebarAfter { @FXML @Getter private ToggleButton mainEditor, syncEditor, export; @FXML @Getter private ToggleGroup itemGroup; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java ================================================ package org.fordes.subtitles.view.controller; import javafx.fxml.FXML; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import lombok.Getter; import org.springframework.stereotype.Component; /** * @author fordes on 2022/1/27 */ @Component public class SidebarBefore { @FXML @Getter private ToggleButton quickStart, subtitleSearch, toolBox; @FXML @Getter private ToggleGroup itemGroup; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java ================================================ package org.fordes.subtitles.view.controller; import javafx.fxml.FXML; import javafx.scene.control.Button; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * @author fordes on 2022/2/1 */ @Slf4j @Component public class SidebarBottom { @FXML @Getter private Button setting; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java ================================================ package org.fordes.subtitles.view.controller; import org.springframework.stereotype.Component; /** * 语音转换 控制器 * * @author fordes on 2022/4/8 */ @Component public class SpeechConversion { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Singleton; import cn.hutool.core.swing.DesktopUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXNodesList; import com.jfoenix.controls.JFXTextField; import com.jfoenix.skins.JFXListViewSkin; import com.sun.javafx.scene.control.VirtualScrollBar; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; import javafx.scene.control.skin.VirtualFlow; import javafx.scene.input.MouseButton; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.constant.CommonConstant; import org.fordes.subtitles.view.constant.StyleClassConstant; import org.fordes.subtitles.view.event.FileOpenEvent; import org.fordes.subtitles.view.event.LoadingEvent; import org.fordes.subtitles.view.event.ToastChooseEvent; import org.fordes.subtitles.view.event.ToastConfirmEvent; import org.fordes.subtitles.view.mapper.SearchCasesMapper; import org.fordes.subtitles.view.model.PO.SearchCases; import org.fordes.subtitles.view.model.search.Cases; import org.fordes.subtitles.view.model.search.Result; import org.fordes.subtitles.view.service.SearchService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @author fordes on 2022/2/6 */ @Slf4j @Component public class SubtitleSearch extends DelayInitController { @FXML private JFXListView listView; @FXML private JFXTextField searchField; @FXML private JFXNodesList nodesList; private ToggleGroup engineGroup; private static final SearchService SERVICE = new SearchService(); private static final Dict SEARCH_KEY = Dict.create(); static final String KEYWORD = "keyword"; private final SearchCasesMapper casesMapper; @Autowired public SubtitleSearch(SearchCasesMapper casesMapper) { this.casesMapper = casesMapper; } @Override public void delay() { //选择默认接口 if (engineGroup.getToggles().isEmpty()) { Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("未找到搜索源", "字幕搜索无法使用!")); searchField.setDisable(true); } else { Toggle engine = CollUtil.getFirst(engineGroup.getToggles()); engine.setSelected(true); searchField.setPromptText(StrUtil .format("从{}搜索", ((SearchCases) engine.getUserData()).getName())); } } @Override public void async() { //读取字幕搜索接口 engineGroup = new ToggleGroup(); casesMapper.selectList(new QueryWrapper<>()).forEach(e -> { ToggleButton engine = new ToggleButton(); engine.getStyleClass().addAll(StyleClassConstant.SUBTITLE_SEARCH_ENGINE, StyleClassConstant.SUBTITLE_SEARCH_ENGINE_ITEM); engine.setToggleGroup(engineGroup); engine.setUserData(e); engine.setTooltip(new Tooltip(e.getName())); engine.setText(e.getIcon()); engine.selectedProperty().addListener((observableValue, aBoolean, t1) -> { if (t1) { SearchCases cases = (SearchCases) engine.getUserData(); searchField.setPromptText(StrUtil.format("从{}搜索", cases.getName())); listView.getItems().clear(); SERVICE.cancel(); nodesList.animateList(false); } }); nodesList.addAnimatedNode(engine); }); //监听搜索服务运行状态,控制loading SERVICE.runningProperty().addListener((observableValue, aBoolean, t1) -> Singleton.get(Stage.class).fireEvent(new LoadingEvent(t1))); //搜索完成,载入新结果 SERVICE.setOnSucceeded(event -> { Result val = SERVICE.getValue(); if (ObjectUtil.isNotNull(val) && !val.getData().isEmpty()) { if (Result.Type.SEARCH.equals(val.getType())) { listView.getItems().clear(); } listView.setUserData(val.getPage()); val.getData().forEach(result -> listView.getItems().add(buildItem(result))); }else { Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("暂无结果", "换一个资源试试吧~", "确定", () -> {})); } }); //搜索出错 SERVICE.setOnFailed(event -> Singleton.get(Stage.class).fireEvent(new ToastChooseEvent("搜索出错", "请等待后尝试重试\n或者前往项目主页反馈", "去反馈", () -> DesktopUtil.browse(CommonConstant.URL_ISSUES)))); //为listview添加skin,反射获取垂直滚动条,监听滚动条判断分页 JFXListViewSkin skin = new JFXListViewSkin<>(listView); listView.setSkin(skin); VirtualFlow virtualFlow = (VirtualFlow) ReflectUtil.getFieldValue(skin, "flow"); VirtualScrollBar vbar = (VirtualScrollBar) ReflectUtil.getFieldValue(virtualFlow, "vbar"); vbar.valueProperty().addListener((observableValue, number, t1) -> { if (t1.floatValue() == 1 && listView.getUserData() != null) { SERVICE.search(Result.Type.PAGE, (Cases) listView.getUserData(), SEARCH_KEY); } }); } /** * 输入框监听,提交新的搜索 * @param event source */ @FXML private void searchBeginHandle(ActionEvent event) { JFXTextField field = (JFXTextField) event.getSource(); if (StrUtil.isNotBlank(field.getText())) { SearchCases cases = (SearchCases) engineGroup.getSelectedToggle().getUserData(); SEARCH_KEY.clear(); SEARCH_KEY.set(KEYWORD, field.getText()); SERVICE.search(Result.Type.SEARCH, cases.getCases(), SEARCH_KEY); } } private StackPane buildItem(Result.Item rsi) { StackPane root = new StackPane(); root.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM); Label caption = new Label(rsi.caption); caption.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM_CAPTION); Label text = new Label(rsi.text); text.getStyleClass().add(StyleClassConstant.SUBTITLE_SEARCH_ITEM_TEXT); root.getChildren().addAll(caption, text); StackPane.setAlignment(caption, Pos.TOP_LEFT); StackPane.setMargin(caption, new Insets(5, 0, 0, 0)); StackPane.setAlignment(text, Pos.BOTTOM_LEFT); StackPane.setMargin(caption, new Insets(0, 0, 5, 0)); root.setUserData(rsi); root.setOnMouseClicked(e -> { if (MouseButton.PRIMARY.equals(e.getButton()) && 2 == e.getClickCount()) { StackPane item = (StackPane) e.getSource(); Result.Item data = (Result.Item)item.getUserData(); if (ObjectUtil.isNull(data.next)) { if (StrUtil.isNotEmpty(data.text)) { Singleton.get(Stage.class).fireEvent(new FileOpenEvent(data.text)); } }else { SERVICE.search(Result.Type.SEARCH, data.next, data.params); } } }); return root; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java ================================================ package org.fordes.subtitles.view.controller; import org.springframework.stereotype.Component; /** * 语音转换 控制器 * * @author fordes on 2022/4/8 */ @Component public class SyncEditor { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/TitleBar.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.lang.Singleton; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.VBox; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.enums.FontIcon; import org.springframework.stereotype.Component; /** * @author fordes on 2022/1/19 */ @Slf4j @Component public class TitleBar { @FXML private Button closed, maximize, minimize; @FXML private VBox root; @FXML private Label title; @FXML private void closed(ActionEvent actionEvent) { //TODO Singleton.get(Stage.class).close(); Platform.exit(); System.exit(0); } @FXML private void maximize(ActionEvent actionEvent) { Stage stage = Singleton.get(Stage.class); stage.setFullScreen(!stage.isFullScreen()); maximize.setText(stage.isFullScreen() ? FontIcon.EXIT_FULL_SCREEN.toString() : FontIcon.FULL_SCREEN.toString()); actionEvent.consume(); } @FXML private void minimize(ActionEvent actionEvent) { Singleton.get(Stage.class).setIconified(true); actionEvent.consume(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/Toast.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.lang.Singleton; import cn.hutool.core.util.StrUtil; import com.jfoenix.controls.JFXButton; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.stage.Stage; import org.fordes.subtitles.view.event.AbstractToastEvent; import org.fordes.subtitles.view.handler.ToastEventHandler; import org.fordes.subtitles.view.handler.ToastHandler; import org.springframework.stereotype.Component; /** * @author fordes on 2022/1/28 */ @Component public class Toast extends DelayInitController { @FXML private JFXButton _perform, _choose1, _choose2; @FXML private Label _caption, _text; @FXML private GridPane root; @Override public void async() { //选择型toast和确认型toast互斥 _perform.visibleProperty().addListener((observableValue, aBoolean, t1) -> { _choose1.setVisible(!t1); _choose2.setVisible(!t1); }); //为stage添加toast事件处理 Singleton.get(Stage.class).addEventHandler(AbstractToastEvent.TOAST_EVENT_TYPE, new ToastEventHandler() { @Override public void onConfirmEvent(String caption, String text, String perform, ToastHandler handler) { _caption.setText(caption); _text.setText(text); if (StrUtil.isNotEmpty(perform)) { _perform.setText(perform); } _perform.setOnAction(actionEvent -> { handler.handle(); root.setVisible(false); }); _perform.setVisible(true); root.setVisible(true); } @Override public void onChooseEvent(String caption, String text, String choose1, String choose2, ToastHandler handler1, ToastHandler handler2) { _caption.setText(caption); _text.setText(text); if (StrUtil.isNotEmpty(choose1)) { _choose1.setText(choose1); } if (StrUtil.isNotEmpty(choose2)) { _choose2.setText(choose2); } _choose1.setOnAction(event -> { handler1.handle(); root.setVisible(false); }); _choose2.setOnAction(event -> { handler2.handle(); root.setVisible(false); }); _perform.setVisible(false); root.setVisible(true); } }); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/ToolBox.java ================================================ package org.fordes.subtitles.view.controller; import org.springframework.stereotype.Component; /** * @author fordes on 2022/2/6 */ @Component public class ToolBox { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java ================================================ package org.fordes.subtitles.view.controller; import cn.hutool.core.lang.Singleton; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.event.FileOpenEvent; import org.fordes.subtitles.view.model.DTO.Video; import org.springframework.stereotype.Component; /** * 语音转换 控制器 * * @author fordes on 2022/4/8 */ @Slf4j @Component public class VoiceConvert extends DelayInitController { private Video video; @Override public void delay() { } @Override public void async() { Singleton.get(Stage.class).addEventHandler(FileOpenEvent.FILE_OPEN_EVENT, fileOpenEvent -> { if (fileOpenEvent.getRecord().getFormat().media) { video = (Video) fileOpenEvent.getRecord(); root.setVisible(true); } }); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java ================================================ package org.fordes.subtitles.view.enums; /** * 编辑工具 事件类型枚举 * * @author fordes on 2022/7/15 */ public enum EditToolEventEnum { SEARCH, //搜索 REPLACE,//替换 JUMP,//跳转 FONT, //字体(样式) TIMELINE, //时间轴 CODE,//编码 REF, //刷新 TRANSLATE //翻译 } ================================================ FILE: src/main/java/org/fordes/subtitles/view/enums/FileEnum.java ================================================ package org.fordes.subtitles.view.enums; import cn.hutool.core.util.StrUtil; import lombok.AllArgsConstructor; import java.util.Arrays; /** * 文件类型枚举 * * @author fordes on 2022/2/9 */ @AllArgsConstructor public enum FileEnum { //视频 MP4("mp4", true, true, false), MKV("mkv", true, true, false), AVI("avi", true, true, false), RMVB("rmvb", true, true, false), TS("ts", true, true, false), //音频 MP3("mp3", true, false, false), FLAC("flac", true, false, false), AAC("aac", true, false, false), //字幕 LRC("lrc", true, false, true), SRT("srt", true, false, true), ASS("ass", true, false, true); public final String suffix; public final boolean support; public final boolean media; public final boolean subtitle; public static final String[] SUPPORT_SUBTITLE = Arrays.stream(FileEnum.values()) .filter(e -> e.subtitle && e.support).map(e -> e.suffix).toArray(String[]::new); public static final String[] SUPPORT_MEDIA = Arrays.stream(FileEnum.values()) .filter(e -> e.media && e.support).map(e -> e.suffix).toArray(String[]::new); public static boolean isMedia(String suffix) { return Arrays.stream(FileEnum.values()) .filter(e -> e.media) .anyMatch(e -> StrUtil.equalsIgnoreCase(e.suffix, suffix)); } public static boolean isSupport(String suffix) { return Arrays.stream(FileEnum.values()) .filter(e -> e.support) .anyMatch(e -> StrUtil.equalsIgnoreCase(e.suffix, suffix)); } public static boolean check(String suffix, boolean isSupport, boolean isMedia, boolean isSubtitle) { FileEnum val = of(suffix); return val != null && (val.support == isSupport) && (val.media == isMedia) && (val.subtitle == isSubtitle); } public static FileEnum of(String name) { for (FileEnum value : FileEnum.values()) { if (StrUtil.equalsIgnoreCase(name, value.suffix)) { return value; } } return null; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/enums/FontIcon.java ================================================ package org.fordes.subtitles.view.enums; import lombok.AllArgsConstructor; /** * 图标枚举 * * @author fordes on 2022/1/23 */ @AllArgsConstructor public enum FontIcon { SCENE_CLOSE("\ue648"), SCENE_MINIMIZE("\ue634"), EXIT_FULL_SCREEN("\ue61f"), FULL_SCREEN("\ue628"), ITEM_START("\ue669"), ITEM_SEARCH("\uec6f"), ITEM_TOOL("\ue64a"), LOGO("\ue69f"), SETTING("\ue711"), CHOOSE_FILE("\ue64e"), ENGINE_DDZM("\ue63b"), ENGINE_ASSRT("\ue609"), ENGINE_ZMK("\ue623"), ENGINE("\ue60f"), PLACE_THE_LEFT("\uec70"), PLACE_THE_RIGHT("\ue61a"), SETTING_PREFERENCES("\ue63c"), SETTING_INTERFACE("\ue62d"), EDIT_BAR_SEARCH("\ue754"), EDIT_BAR_REPLACE("\ue674"), EDIT_BAR_JUMP("\ue695"), EDIT_BAR_FONT("\ue61d"), EDIT_BAR_HIDE("\ue60b"), EDIT_BAR_TIMELINE("\ue64f"), EDIT_BAR_CODE("\ue629"), EDIT_BAR_REF("\ue62c"), EDIT_BAR_OPTION("\ue86c"), EDIT_BAR_REPLACE_ITEM("\ue63e"), EDIT_BAR_REPLACE_ALL("\ue642"), SWITCH_OFF_LIGHT("\ue612"), SWITCH_ON_LIGHT("\ue611"), SWITCH_OFF_DARK("\ue613"), SWITCH_ON_DARK("\ue615"), EDIT_BAR_TRANSLATE("\ue6fb"); private final String unicode; @Override public String toString() { return unicode; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java ================================================ package org.fordes.subtitles.view.enums; import com.baomidou.mybatisplus.annotation.IEnum; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum ServiceProvider implements IEnum { BAIDU("百度"), TENCENT("腾讯"), ALI("阿里"), HUOSHAN("火山"); private final String desc; @Override public String toString() { return this.desc; } @Override public String getValue() { return this.name(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/enums/ServiceType.java ================================================ package org.fordes.subtitles.view.enums; import com.baomidou.mybatisplus.annotation.IEnum; import lombok.AllArgsConstructor; import lombok.Getter; /** * 服务类型枚举 * * @author fordes on 2022/4/17 */ @Getter @AllArgsConstructor public enum ServiceType implements IEnum { VOICE("语音转写"), TRANSLATE("翻译"); private final String desc; @Override public String toString() { return this.getDesc(); } public String getValue() { return this.name(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java ================================================ package org.fordes.subtitles.view.enums; import lombok.Getter; /** * 7zip结束码枚举 * * @author fordes on 2021/1/7 */ @Getter public enum SevenZipEnum { NORMAL(0, "未发生错误"), WARNING(1,"警告,发生部分错误"), FATAL_ERROR(2, "致命错误"), COMMAND_ERROR(7, "命令错误"), OUT_OF_MEMORY_ERROR(8, "内存不足"), TERMINATION(255, "操作终止"), UNKNOWN_ERROR(-1, "未知错误"); SevenZipEnum(int code, String status) { this.code = code; this.status = status; } private final int code; private final String status; public static String getStatus(int code){ switch (code){ case 0: return NORMAL.status; case 1: return WARNING.status; case 2: return FATAL_ERROR.status; case 7: return COMMAND_ERROR.status; case 8: return OUT_OF_MEMORY_ERROR.status; case 255: return TERMINATION.status; default: return UNKNOWN_ERROR.status; } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java ================================================ package org.fordes.subtitles.view.event; import javafx.event.Event; import javafx.event.EventType; import org.fordes.subtitles.view.handler.ToastEventHandler; /** * @author fordes on 2022/2/2 */ public abstract class AbstractToastEvent extends Event { public static final String CONFIRM = "确定"; public static final String CANCEL = "取消"; public static final EventType TOAST_EVENT_TYPE = new EventType(ANY); public AbstractToastEvent(EventType eventType) { super(eventType); } public abstract void invokeHandler(ToastEventHandler handler); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java ================================================ package org.fordes.subtitles.view.event; import javafx.event.Event; import javafx.event.EventType; import javafx.scene.control.ToggleButton; import lombok.Getter; import lombok.NonNull; import org.fordes.subtitles.view.enums.EditToolEventEnum; import org.fordes.subtitles.view.model.DTO.Subtitle; import org.fxmisc.richtext.StyleClassedTextArea; /** * 编辑工具 事件 * * @author fordes on 2022/7/15 */ public class EditToolEvent extends Event { public static final EventType EVENT_TYPE = new EventType<>(ANY, "editToolEvent"); @Getter private final StyleClassedTextArea source; @Getter private final Subtitle subtitle; @Getter private final ToggleButton editMode; @Getter private final EditToolEventEnum type; public EditToolEvent(@NonNull StyleClassedTextArea source, @NonNull Subtitle subtitle, @NonNull ToggleButton editMode, @NonNull EditToolEventEnum type) { super(EVENT_TYPE); this.source = source; this.subtitle = subtitle; this.editMode = editMode; this.type = type; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java ================================================ package org.fordes.subtitles.view.event; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Singleton; import javafx.event.Event; import javafx.event.EventType; import javafx.stage.Stage; import lombok.Getter; import org.fordes.subtitles.view.model.PO.FileRecord; import org.fordes.subtitles.view.utils.FileUtils; import java.io.File; import java.io.IOException; /** * @author fordes on 2022/4/8 */ public class FileOpenEvent extends Event { public static final EventType FILE_OPEN_EVENT = new EventType(ANY, "fileOpenEvent"); @Getter private FileRecord record; public FileOpenEvent(File openFile) { super(FILE_OPEN_EVENT); try { this.record = FileUtils.readFileInfo(openFile); }catch (IOException e) { Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("出错了","打开文件失败!")); } } public FileOpenEvent(String filePath) { super(FILE_OPEN_EVENT); try { this.record = FileUtils.readFileInfo(FileUtil.file(filePath)); }catch (IOException e) { Singleton.get(Stage.class).fireEvent(new ToastConfirmEvent("出错了","打开文件失败!")); } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java ================================================ package org.fordes.subtitles.view.event; import javafx.event.Event; import javafx.event.EventType; import lombok.Getter; /** * loading事件 * * @author fordes on 2022/7/20 */ public class LoadingEvent extends Event { public final static EventType EVENT_TYPE = new EventType<>(ANY, "loadingEvent"); @Getter private final boolean alive; public LoadingEvent(boolean alive) { super(EVENT_TYPE); this.alive = alive; } public LoadingEvent() { super(EVENT_TYPE); this.alive = false; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java ================================================ package org.fordes.subtitles.view.event; import javafx.event.Event; import javafx.event.EventType; /** * 主题切换事件 * * @author fordes on 2022/4/13 */ public class ThemeChangeEvent extends Event { public static final EventType EVENT_TYPE = new EventType(ANY, "themeChangeEvent"); private Boolean dark; public Boolean isDark() { return dark; } public ThemeChangeEvent(Boolean dark) { super(EVENT_TYPE); this.dark = dark; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java ================================================ package org.fordes.subtitles.view.event; import javafx.event.EventType; import org.fordes.subtitles.view.handler.ToastEventHandler; import org.fordes.subtitles.view.handler.ToastHandler; /** * toast选择事件 * * @author fordes on 2022/2/2 */ public class ToastChooseEvent extends AbstractToastEvent { public static final EventType TOAST_CHOOSE_EVENT_TYPE = new EventType(TOAST_EVENT_TYPE, "toastChooseEvent"); private final String caption; private final String text; private final String choose1; private final String choose2; private final ToastHandler handler1; private final ToastHandler handler2; public ToastChooseEvent(String caption, String text, String choose1, String choose2, ToastHandler handler1, ToastHandler handler2) { super(TOAST_CHOOSE_EVENT_TYPE); this.caption = caption; this.text = text; this.choose1 = choose1; this.choose2 = choose2; this.handler1 = handler1; this.handler2 = handler2; } public ToastChooseEvent(String caption, String text, String choose1, ToastHandler handler1) { super(TOAST_CHOOSE_EVENT_TYPE); this.caption = caption; this.text = text; this.choose1 = choose1; this.choose2 = AbstractToastEvent.CANCEL; this.handler1 = handler1; this.handler2 = () -> {}; } @Override public void invokeHandler(ToastEventHandler handler) { handler.onChooseEvent(caption, text, choose1, choose2, handler1, handler2); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java ================================================ package org.fordes.subtitles.view.event; import javafx.event.EventType; import org.fordes.subtitles.view.handler.ToastEventHandler; import org.fordes.subtitles.view.handler.ToastHandler; /** * @author fordes on 2022/2/2 */ public class ToastConfirmEvent extends AbstractToastEvent { public static final EventType TOAST_CONFIRM_EVENT_TYPE = new EventType(TOAST_EVENT_TYPE, "confirmToastEvent"); private final String caption; private final String text; private final String perform; private final ToastHandler handler; public ToastConfirmEvent(String caption, String text, String perform, ToastHandler handler) { super(TOAST_CONFIRM_EVENT_TYPE); this.caption = caption; this.text = text; this.perform = perform; this.handler = handler; } public ToastConfirmEvent(String caption, String text) { super(TOAST_CONFIRM_EVENT_TYPE); this.caption = caption; this.text = text; this.perform = AbstractToastEvent.CONFIRM; this.handler = () -> {}; } @Override public void invokeHandler(ToastEventHandler handler) { handler.onConfirmEvent(caption, text, perform, this.handler); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java ================================================ package org.fordes.subtitles.view.event; import javafx.event.Event; import javafx.event.EventType; import lombok.Getter; /** * 翻译服务事件 * * @author fordes on 2022/8/1 */ public class TranslateEvent extends Event { public static final EventType EVENT_TYPE = new EventType<>(ANY, "translateEvent"); public static final String SUCCESS = "翻译完成"; public static final String FAIL = "翻译失败"; public TranslateEvent(EventType eventType) { super(eventType); } @Getter private String msg; @Getter private String detail; public TranslateEvent(String msg, String detail) { super(EVENT_TYPE); this.msg = msg; this.detail = detail; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java ================================================ package org.fordes.subtitles.view.factory; import org.fordes.subtitles.view.service.translate.TranslateService; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author fordes on 2022/7/11 */ public class TranslateServiceFactory { private static final Map services = new ConcurrentHashMap<>(); public static TranslateService getService(String provider) { return services.getOrDefault(provider, null); } public static void register(TranslateService service, String provider) { services.put(provider, service); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java ================================================ package org.fordes.subtitles.view.handler; /** * @author fordes on 2022/7/27 */ @FunctionalInterface public interface CallBackHandler { void handle(T value); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java ================================================ package org.fordes.subtitles.view.handler; import javafx.event.EventHandler; import org.fordes.subtitles.view.event.EditToolEvent; /** * 编辑工具 事件处理器 * * @author fordes on 2022/7/15 */ public abstract class EditToolEventHandler implements EventHandler { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java ================================================ package org.fordes.subtitles.view.handler; import javafx.event.EventHandler; import org.fordes.subtitles.view.event.FileOpenEvent; /** * @author fordes on 2022/4/8 */ public abstract class FileOpenEventHandler implements EventHandler { public final static String ERROR_MESSAGE = "文件打开失败"; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java ================================================ package org.fordes.subtitles.view.handler; import javafx.event.EventHandler; import org.fordes.subtitles.view.event.AbstractToastEvent; /** * toast事件抽象 * * @author fordes on 2022/2/2 */ public abstract class ToastEventHandler implements EventHandler { /** * 确认型 toast事件 * @param caption 标题 * @param text 内容 * @param perform 确认按钮文本 * @param handler 回调 */ public abstract void onConfirmEvent(String caption, String text, String perform, ToastHandler handler); /** * 选择型 toast事件 * @param caption 标题 * @param text 内容 * @param choose1 选择1 * @param choose2 选择2 * @param handler1 选择1回调 * @param handler2 选择2回调 */ public abstract void onChooseEvent(String caption, String text, String choose1, String choose2, ToastHandler handler1, ToastHandler handler2); @Override public void handle(AbstractToastEvent event) { event.invokeHandler(this); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java ================================================ package org.fordes.subtitles.view.handler; /** * toast回调事件处理器接口 * * @author fordes on 2022/2/2 */ @FunctionalInterface public interface ToastHandler { void handle(); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java ================================================ package org.fordes.subtitles.view.mapper; import cn.hutool.core.lang.Dict; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo; import org.fordes.subtitles.view.model.PO.ServiceInterface; import org.fordes.subtitles.view.model.PO.Version; import java.util.List; /** * @author fordes on 2022/4/17 */ @Mapper public interface InterfaceMapper extends BaseMapper { List serviceInfo(@Param("type") String type); List getVersions(@Param("type") String serviceType, @Param("provider") String provider); List getLanguageList(); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java ================================================ package org.fordes.subtitles.view.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.fordes.subtitles.view.model.PO.Language; @Mapper public interface LanguageMapper extends BaseMapper { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java ================================================ package org.fordes.subtitles.view.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.fordes.subtitles.view.model.PO.SearchCases; /** * @author fordes on 2022/2/15 */ @Mapper public interface SearchCasesMapper extends BaseMapper { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java ================================================ package org.fordes.subtitles.view.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.fordes.subtitles.view.model.PO.Version; /** * @author fordes on 2022/4/17 */ @Mapper public interface VersionMapper extends BaseMapper { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java ================================================ package org.fordes.subtitles.view.model.DTO; import lombok.Data; import lombok.EqualsAndHashCode; import org.fordes.subtitles.view.model.PO.ServiceInterface; import org.fordes.subtitles.view.model.PO.Version; /** * @author fordes on 2022/4/20 */ @Data @EqualsAndHashCode(callSuper = false) public class AvailableServiceInfo extends ServiceInterface { private Version versionInfo; @Override public String toString() { return getProvider().getDesc() + getType().getDesc(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java ================================================ package org.fordes.subtitles.view.model.DTO; /** * @author fordes on 2022/7/19 */ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import org.fordes.subtitles.view.model.PO.FileRecord; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; /** * @author fordes on 2021/6/30 */ @Data @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) public class Subtitle extends FileRecord { private TimedTextFile timedTextFile; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java ================================================ package org.fordes.subtitles.view.model.DTO; import lombok.Builder; import lombok.Data; /** * 翻译 * * @author fordes on 2022/7/27 */ @Data @Builder public class TranslateResult { private Integer serial; private boolean success; private String data; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/DTO/Video.java ================================================ package org.fordes.subtitles.view.model.DTO; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import org.fordes.subtitles.view.model.PO.FileRecord; /** * 视频类 * * @author fordes on 2020/12/4 */ @Data @NoArgsConstructor @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) public class Video extends FileRecord { /** * 帧宽 */ private int width; /** * 帧高 */ private int height; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java ================================================ package org.fordes.subtitles.view.model.PO; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.experimental.Accessors; import org.fordes.subtitles.view.enums.FileEnum; import java.io.File; import java.util.Date; /** * 文件抽象类 * * @author fordes on 2020/12/4 */ @Data @Accessors(chain = true) public class FileRecord { @JsonIgnore private File file; /** * 名称 */ private String file_name; /** * 格式 */ private FileEnum format; /** * 语言 */ private String language; /** * 文件路径 */ private String path; /** * 文件字节大小 */ private Long size_byte; /** * 文件大小 */ private String size; /** * 长度,字幕为时间轴起止,视频为时长 */ private Long duration; /** * 编码 */ private String charset; /** * 文件最后修改时间 */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date file_modify_time; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/PO/Interface.java ================================================ package org.fordes.subtitles.view.model.PO; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; @Data @TableName(value = "interface") public class Interface implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField(value = "provider") private String provider; @TableField(value = "\"type\"") private String type; @TableField(value = "auth") private String auth; @TableField(value = "page") private String page; @TableField(value = "\"template\"") private String template; private static final long serialVersionUID = 1L; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/PO/Language.java ================================================ package org.fordes.subtitles.view.model.PO; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.List; @Data @Accessors(chain = true) @NoArgsConstructor @TableName(value = "\"language\"") public class Language implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Integer id; private String name; @TableField(exist = false) private String code; @TableField(exist = false) private boolean general; @TableField(exist = false) private List _target; @TableField(exist = false) private List target; @TableField("huoshan") private String huoshan; public String toString() { return this.name; } private static final long serialVersionUID = 1L; public static final String COL_ID = "id"; public static final String COL_TYPE = "type"; public static final String COL_NAME = "name"; public static final String COL_GENERAL = "general"; public static final String TARGET = "_target"; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java ================================================ package org.fordes.subtitles.view.model.PO; import cn.hutool.json.JSONUtil; import lombok.Data; import lombok.experimental.Accessors; import org.fordes.subtitles.view.model.search.Cases; /** * @author fordes on 2022/2/15 */ @Data @Accessors(chain = true) public class SearchCases { /** * 自增主键 */ private Integer id; /** * 名称 */ private String name; /** * 图标 {@link org.fordes.subtitles.view.enums.FontIcon} */ private String icon; /** * 用例 */ private Cases cases; /** * 备注 */ private String remark; public void setCases(String cases) { this.cases = JSONUtil.toBean(cases, Cases.class); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java ================================================ package org.fordes.subtitles.view.model.PO; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.enums.ServiceType; import java.io.Serializable; /** * @author fordes on 2022/4/19 */ @Data @Accessors(chain = true) @TableName(value = "interface") public class ServiceInterface implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 服务提供商 {@link ServiceProvider} */ @TableField(value = "provider") private ServiceProvider provider; /** * 服务类型 {@link ServiceType} */ @TableField(value = "type") private ServiceType type; /** * 授权信息 */ @TableField(value = "auth") private String auth; /** * 授权信息模板 */ @TableField(value = "template") private String template; /** * 主页 */ @TableField(value = "page") private String page; private static final long serialVersionUID = 1L; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/PO/Version.java ================================================ package org.fordes.subtitles.view.model.PO; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; /** * @author fordes on 2022/4/17 */ @Data @Accessors(chain = true) @TableName(value = "version") public class Version implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField(value = "interface_id") private Integer interfaceId; @TableField(value = "\"name\"") private String name; @TableField(value = "concurrent") private Integer concurrent; @TableField(value = "carrying") private Integer carrying; @TableField(value = "server_url") private String serverUrl; @TableField(value = "remark") private String remark; public String toString() { return this.name; } private static final long serialVersionUID = 1L; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/search/Cases.java ================================================ package org.fordes.subtitles.view.model.search; import cn.hutool.http.ContentType; import lombok.Builder; import java.io.Serializable; import java.util.Map; /** * @author fordes on 2022/3/28 */ @Builder public class Cases implements Serializable { public static final String CAPTION = "caption"; public static final String TEXT = "text"; public static final String PAGE = "page"; public String[] keys; public Object url; public ContentType type; public Map params; public Cases next; public void setType(String val) { this.type = ContentType.valueOf(val); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/search/Engine.java ================================================ package org.fordes.subtitles.view.model.search; import lombok.Data; import lombok.experimental.Accessors; /** * @author fordes on 2022/2/12 */ @Data @Accessors(chain = true) public class Engine { private String id; private String name; private String url; private Cases cases; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/search/Result.java ================================================ package org.fordes.subtitles.view.model.search; import cn.hutool.core.map.MapUtil; import lombok.Builder; import lombok.Data; import java.util.List; import java.util.Map; /** * @author fordes on 2022/2/12 */ @Data @Builder public class Result { private Type type; private Cases page; private List data; @Builder public static class Item { public Cases next; public String caption; public String text; public boolean isFile = false; public Map params = MapUtil.newHashMap(); } public static enum Type { //普通搜索 SEARCH(), //分页 PAGE() } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/model/search/Selector.java ================================================ package org.fordes.subtitles.view.model.search; import java.io.Serializable; /** * 字段解析器 * @author fordes on 2022/3/28 */ public class Selector implements Serializable { /** * 唯一性标识 * false:按条件提取结果集,true:按条件提取唯一结果 */ public boolean only = false; /** * 正则提取,提取匹配正则的内容 * 高优先级 */ public String regular; /** * 内容格式化模板,参考{@see cn.hutool.core.util.StrUtil.format()} */ public String format; /** * key选择 多层级使用"."连接 如:a.c.b */ public String jsonKey; /** * css选择器 参考Jsoup css选择器 * 高优先级 */ public String css; /** * 属性选择 提取指定属性,为空时使用text() */ public String attr; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/ConfigService.java ================================================ package org.fordes.subtitles.view.service; import com.baomidou.mybatisplus.extension.service.IService; import org.fordes.subtitles.view.config.ApplicationConfig; /** * @author fordes on 2022/4/17 */ public interface ConfigService extends IService { } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java ================================================ package org.fordes.subtitles.view.service.Impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.AllArgsConstructor; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.enums.ServiceType; import org.fordes.subtitles.view.mapper.InterfaceMapper; import org.fordes.subtitles.view.mapper.VersionMapper; import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo; import org.fordes.subtitles.view.model.PO.ServiceInterface; import org.fordes.subtitles.view.model.PO.Version; import org.fordes.subtitles.view.service.InterfaceService; import org.springframework.stereotype.Service; import java.util.List; /** * 接口服务 * * @author fordes on 2022/4/17 */ @Service @AllArgsConstructor public class InterfaceServiceImpl extends ServiceImpl implements InterfaceService { private final InterfaceMapper interfaceMapper; private final VersionMapper versionMapper; // private final DictMapper dictMapper; @Override public List getVersions(ServiceType type, ServiceProvider provider) { return interfaceMapper.getVersions(type.name(), provider.name()); } @Override public ServiceInterface getInterface(ServiceType type, ServiceProvider provider) { return interfaceMapper.selectOne(new LambdaQueryWrapper() .eq(ServiceInterface::getType, type) .eq(ServiceInterface::getProvider, provider)); } @Override public List getAvailableService(ServiceType type) { return interfaceMapper.serviceInfo(type.name()); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/InterfaceService.java ================================================ package org.fordes.subtitles.view.service; import com.baomidou.mybatisplus.extension.service.IService; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.enums.ServiceType; import org.fordes.subtitles.view.model.DTO.AvailableServiceInfo; import org.fordes.subtitles.view.model.PO.ServiceInterface; import org.fordes.subtitles.view.model.PO.Version; import java.util.List; /** * 接口服务 * * @author fordes on 2022/4/17 */ public interface InterfaceService extends IService { /** * 获取指定接口的版本信息 * * @param type 服务类型 {@link ServiceType} * @param provider 服务提供商 {@link ServiceProvider} * @return { @link Version} */ List getVersions(ServiceType type, ServiceProvider provider); ServiceInterface getInterface(ServiceType type, ServiceProvider provider); /** * 获取可用的服务接口 * * @param type 服务类型 {@link ServiceType} * @return {@link AvailableServiceInfo} */ List getAvailableService(ServiceType type); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/SearchService.java ================================================ package org.fordes.subtitles.view.service; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.ContentType; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import javafx.concurrent.Service; import javafx.concurrent.Task; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.constant.CommonConstant; import org.fordes.subtitles.view.model.search.Cases; import org.fordes.subtitles.view.model.search.Result; import org.fordes.subtitles.view.utils.ArchiveUtil; import org.fordes.subtitles.view.utils.search.ParsingFactory; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; /** * 在线字幕搜索服务 * * @author fordes on 2022/2/15 */ @Slf4j public class SearchService extends Service { private Result.Type type; private Cases cases; private Map params = MapUtil.newHashMap(); @Override protected Task createTask() { return new Task<>() { @Override protected Result call() { Result result = Result.builder() .type(type) .data(CollUtil.newArrayList()).build(); try { List paramList = new ArrayList<>(params.size()); if (null != cases.keys) { for (String key : cases.keys) { paramList.add(params.get(key)); } } String url = StrUtil.format((CharSequence) cases.url, paramList.toArray()); HttpResponse response = HttpUtil .createGet(url, true) .execute(); if (ObjectUtil.isEmpty(cases.next)) { File outFile = response.completeFileNameFromHeader(FileUtil.mkdir(CommonConstant.DOWNLOAD_PATH)); FileUtil.writeFromStream(response.bodyStream(), outFile); log.debug("下载文件成功!{}", outFile.getPath()); for (File l : ArchiveUtil.unArchiveToCurrentPath(outFile)) { result.getData().add(Result.Item.builder() .caption(l.getName()) .text(l.getPath()) .isFile(true) .build()); } } else { ContentType contentType = ContentType.get(StrUtil.trimStart(response.body())); if (contentType != null) { //根据类型,创建解析器 ParsingFactory parsing = new ParsingFactory(response.body(), contentType); //遍历解析,获取结果 Map> displayMap = MapUtil.newHashMap(); Map otherMap = MapUtil.newHashMap(); cases.params.forEach((k, v) -> { switch (k) { case Cases.CAPTION: case Cases.TEXT: displayMap.put(k, Convert.toList(String.class, parsing.getResult(v))); break; case Cases.PAGE: if (ObjectUtil.isNotEmpty(parsing.getResult(v))) { result.setPage(Cases.builder() .keys(cases.keys) .next(cases.next) .type(cases.type) .params(cases.params) .url(parsing.getResult(v)) .build()); } break; default: otherMap.put(k, parsing.getResult(v)); } }); //拼装结果 List captions = displayMap.get("caption"); List texts = displayMap.get("text"); for (int i = 0; i < captions.size(); i++) { result.getData().add(Result.Item.builder() .caption(CollUtil.get(captions, i)) .text(CollUtil.get(texts, i)) .params(MapUtil.newHashMap()) .next(cases.next) .build()); } otherMap.forEach((k, v) -> { if (v instanceof Collection) { List list = Convert.toList(String.class, v); for (int i = 0; i < result.getData().size(); i++) { result.getData().get(i).params.put(k, list.get(i)); } } else { for (Result.Item value : result.getData()) { value.params.put(k, v); } } }); } } } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); throw new RuntimeException(); } return result; } }; } public void search(Result.Type type, Cases cases, Map params) { this.type = type; this.cases = cases; this.params = params; this.restart(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java ================================================ package org.fordes.subtitles.view.service.translate; import cn.hutool.core.map.MapUtil; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.factory.TranslateServiceFactory; import org.fordes.subtitles.view.model.DTO.TranslateResult; import org.fordes.subtitles.view.model.PO.Version; import org.fordes.subtitles.view.service.translate.thread.AliTranslateThread; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ThreadPoolExecutor; /** * @author fordes on 2022/7/25 */ @Slf4j @Service public class AliTranslateService extends TranslateService implements InitializingBean { static final String APP_ID = "Accesskey ID"; static final String APP_KEY = "AccessKey Secret"; @Override public void afterPropertiesSet() { TranslateServiceFactory.register(this, ServiceProvider.ALI.name()); } @Override public Callable createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map config) { String id = MapUtil.getStr(config, APP_ID); String secret = MapUtil.getStr(config, APP_KEY); return new AliTranslateThread(id, secret, serial, version.getServerUrl(), target, original, segment); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java ================================================ package org.fordes.subtitles.view.service.translate; import cn.hutool.core.map.MapUtil; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.factory.TranslateServiceFactory; import org.fordes.subtitles.view.model.DTO.TranslateResult; import org.fordes.subtitles.view.model.PO.Version; import org.fordes.subtitles.view.service.translate.thread.BaiduTranslateThread; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ThreadPoolExecutor; /** * @author fordes on 2022/7/11 */ @Service public class BaiduTranslateService extends TranslateService implements InitializingBean { static final String APP_ID = "APP_ID"; static final String APP_KEY = "APP_KEY"; @Override public void afterPropertiesSet() { TranslateServiceFactory.register(this, ServiceProvider.BAIDU.name()); } @Override public Callable createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map config) { String app_id = MapUtil.getStr(config, APP_ID); String app_key = MapUtil.getStr(config, APP_KEY); return new BaiduTranslateThread(app_id, app_key, serial, version.getServerUrl(), target, original, segment); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java ================================================ package org.fordes.subtitles.view.service.translate; import cn.hutool.core.map.MapUtil; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.factory.TranslateServiceFactory; import org.fordes.subtitles.view.model.DTO.TranslateResult; import org.fordes.subtitles.view.model.PO.Version; import org.fordes.subtitles.view.service.translate.thread.HuoShanTranslateThread; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ThreadPoolExecutor; /** * @author fordes on 2022/7/31 */ @Service public class HuoShanTranslateService extends TranslateService implements InitializingBean { static final String AccessKeyID = "AccessKeyID"; static final String SecretAccessKey = "SecretAccessKey"; @Value("${service.translate.huoshan.region: cn-north-1}") private String region; @Value("${service.translate.huoshan.version-date: 2020-06-01}") private String versionDate; @Override public void afterPropertiesSet() { TranslateServiceFactory.register(this, ServiceProvider.HUOSHAN.name()); } @Override public Callable createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map config) { return new HuoShanTranslateThread(versionDate, region, MapUtil.getStr(config, AccessKeyID), MapUtil.getStr(config, SecretAccessKey), serial, version.getServerUrl(), target, original, segment); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java ================================================ package org.fordes.subtitles.view.service.translate; import cn.hutool.core.map.MapUtil; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.factory.TranslateServiceFactory; import org.fordes.subtitles.view.model.DTO.TranslateResult; import org.fordes.subtitles.view.model.PO.Version; import org.fordes.subtitles.view.service.translate.thread.TencentTranslateThread; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ThreadPoolExecutor; /** * @author fordes on 2022/7/11 */ @Service public class TencentTranslateService extends TranslateService implements InitializingBean { static final String SECRET_ID = "Secret Id"; static final String SECRET_KEY = "Secret Key"; @Value("${service.translate.tencent.region: ap-shanghai}") private String region; @Override public void afterPropertiesSet() { TranslateServiceFactory.register(this, ServiceProvider.TENCENT.name()); } @Override public Callable createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map config) { String id = MapUtil.getStr(config, SECRET_ID); String key = MapUtil.getStr(config, SECRET_KEY); return new TencentTranslateThread(id ,key, region, serial, version.getServerUrl(), target, original, segment); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java ================================================ package org.fordes.subtitles.view.service.translate; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.Singleton; import cn.hutool.core.util.StrUtil; import javafx.application.Platform; import javafx.stage.Stage; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.event.LoadingEvent; import org.fordes.subtitles.view.event.TranslateEvent; import org.fordes.subtitles.view.model.DTO.Subtitle; import org.fordes.subtitles.view.model.DTO.TranslateResult; import org.fordes.subtitles.view.model.PO.Version; import org.fordes.subtitles.view.utils.TranslateUtil; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.*; /** * @author fordes on 2022/7/29 */ @Slf4j @Service public abstract class TranslateService { @Resource private ThreadPoolExecutor globalExecutor; @Async public void translate(Subtitle subtitle, String target, String original, Version version, boolean mode, Map config) { TimeInterval interval = DateUtil.timer(); Singleton.get(Stage.class).fireEvent(new LoadingEvent(true)); //根据接口限制,重设线程池 int threadNum = Math.min(globalExecutor.getMaximumPoolSize(), version.getConcurrent() - 1); globalExecutor.setCorePoolSize(threadNum); globalExecutor.setMaximumPoolSize(threadNum); //根据接口限制对数据整合分段 List segmented = TranslateUtil.segmented(subtitle, version.getCarrying()); //延迟队列 DelayQueue queue = new DelayQueue<>(); for (int i = 0; i < segmented.size(); i++) { queue.put(new Segment(segmented.get(i), i, ((i + 1) % version.getCarrying()) - 1)); } //添加任务, 提交至线程池 Collection> futures = CollUtil.newArrayList(); try { while (!queue.isEmpty()) { Segment part = queue.take(); Integer serial = part.getSerial(); String segment = part.getData(); Future task = globalExecutor .submit(createTask(globalExecutor, serial, segment, target, original, version, config)); futures.add(task); } //遍历获取结果 for (Future e : futures) { TranslateResult item = e.get(); if (item.isSuccess()) { segmented.set(item.getSerial(), item.getData()); } else { throw new RuntimeException(item.getData()); } } //合并结果 TranslateUtil.reduction(subtitle, segmented, mode); } catch (Exception ex) { log.error(ExceptionUtil.stacktraceToString(ex)); // ApplicationInfo.stage.fireEvent(new ToastConfirmEvent("翻译失败", ex.getMessage())); Platform.runLater(() -> Singleton.get(Stage.class).fireEvent(new TranslateEvent(TranslateEvent.FAIL, ex.getMessage()))); return; } finally { log.debug("翻译线程结束,耗时:{} ms", interval.intervalMs()); } // ApplicationInfo.stage.fireEvent(new ToastConfirmEvent("翻译完成", StrUtil.format("总耗时:{} ms", interval.intervalMs()))); // Platform.runLater(() -> ApplicationInfo.stage.fireEvent(new LoadingEvent(false))); Platform.runLater(() -> Singleton.get(Stage.class).fireEvent(new TranslateEvent(TranslateEvent.SUCCESS, StrUtil.format("总耗时:{} ms", interval.intervalMs())))); } /** * 创建翻译线程 * * @param executor 线程池 {@link ThreadPoolExecutor} * @param serial 序号,用于再整合结果时维持内容顺序 * @param segment 待翻译内容 * @param target 目标语言 * @param original 源语言 * @param version 接口版本 * @param config 接口配置 * @return 线程 */ public abstract Callable createTask(ThreadPoolExecutor executor, int serial, String segment, String target, String original, Version version, Map config); static class Segment implements Delayed { private final long executeTime; @Getter private final Integer serial; @Getter private final String data; public Segment(String data, Integer serial, long delay) { this.data = data; this.serial = serial; this.executeTime = System.nanoTime()+ TimeUnit.NANOSECONDS.convert(delay, TimeUnit.SECONDS); } @Override public long getDelay(TimeUnit unit) { return unit.convert(this.executeTime - System.nanoTime(), TimeUnit.NANOSECONDS); } @Override public int compareTo(Delayed o) { Segment that = (Segment) o; return Long.compare(executeTime, that.executeTime); } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java ================================================ package org.fordes.subtitles.view.service.translate.thread; import cn.hutool.core.codec.Base64; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; import cn.hutool.crypto.digest.HMac; import cn.hutool.crypto.digest.HmacAlgorithm; import cn.hutool.crypto.digest.MD5; import cn.hutool.http.*; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.model.DTO.TranslateResult; import java.net.URL; import java.util.UUID; import java.util.concurrent.Callable; /** * @author fordes on 2022/7/26 */ @Slf4j public class AliTranslateThread extends TranslateThread implements Callable { static final String CONTENT_MD5 = "Content-MD5"; static final String CONTENT_TYPE = "application/json;chrset=utf-8"; static final String X_ACS_SIGNATURE_NONCE = "x-acs-signature-nonce"; static final String X_ACS_SIGNATURE_METHOD = "x-acs-signature-method"; static final String X_ACS_VERSION = "x-acs-version"; static final String HMAC_SHA1 = "HMAC-SHA1"; static final String VERSION = "2019-01-02"; private final String ak_id; private final String ak_secret; public AliTranslateThread(String ak_id, String ak_secret, Integer serial, String serviceURL, String target, String original, String content) { super(serial, serviceURL, target, original, content); this.ak_id = ak_id; this.ak_secret = ak_secret; } @Override public TranslateResult call() { TimeInterval interval = DateUtil.timer(); URL url = URLUtil.url(serviceURL); Dict param = Dict.of( "FormatType", "text", "SourceLanguage", original, "TargetLanguage", target, "SourceText", content, "Scene", "general" ); String postBody = JSONUtil.parseObj(param).toString(); String bodyMd5 = Base64.encode(MD5.create().digest(postBody)); String uuid = UUID.randomUUID().toString(); String date = DateTime.now().toString(DatePattern.HTTP_DATETIME_FORMAT); String stringToSign = Method.POST.name() + StrUtil.LF + ContentType.JSON.getValue() + StrUtil.LF + bodyMd5 + StrUtil.LF + CONTENT_TYPE + StrUtil.LF + date + StrUtil.LF + X_ACS_SIGNATURE_METHOD + StrUtil.COLON + HMAC_SHA1 + StrUtil.LF + X_ACS_SIGNATURE_NONCE + StrUtil.COLON + uuid + StrUtil.LF + X_ACS_VERSION + StrUtil.COLON + VERSION + StrUtil.LF + url.getFile(); String signature = new HMac(HmacAlgorithm.HmacSHA1, ak_secret.getBytes()).digestBase64(stringToSign, false); String authHeader = "acs " + ak_id + ":" + signature; HttpResponse response = HttpUtil.createPost(serviceURL) .header(Header.ACCEPT, ContentType.JSON.getValue()) .header(Header.CONTENT_TYPE, CONTENT_TYPE) .header(CONTENT_MD5, bodyMd5) .header(Header.DATE, date) .header(Header.HOST, url.getHost()) .header(Header.AUTHORIZATION, authHeader) .header(X_ACS_SIGNATURE_NONCE, uuid) .header(X_ACS_SIGNATURE_METHOD, HMAC_SHA1) .header(X_ACS_VERSION, VERSION) .setFollowRedirects(true) .body(postBody) .execute(); JSONObject resp = JSONUtil.parseObj(response.body()); TranslateResult result = TranslateResult.builder().serial(serial).build(); if (response.isOk() && resp.containsKey("Data")) { result.setSuccess(true); result.setData(resp.getJSONObject("Data").getStr("Translated")); } else { result.setSuccess(false); result.setData(resp.getStr("errorMsg")); } log.debug("序号:{} 请求 {},耗时:{} ms", serial,result.isSuccess()? "成功":"失败", interval.intervalMs()); // log.debug(resp.toStringPretty()); return result; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java ================================================ package org.fordes.subtitles.view.service.translate.thread; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.crypto.digest.MD5; import cn.hutool.http.ContentType; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.model.DTO.TranslateResult; import java.util.List; import java.util.concurrent.Callable; import java.util.stream.Collectors; /** * @author fordes on 2022/7/27 */ @Slf4j public class BaiduTranslateThread extends TranslateThread implements Callable { static final String SALT = "subview-proxy"; private final String app_id; private final String app_key; public BaiduTranslateThread(String app_id, String app_key, Integer serial, String serviceURL, String target, String original, String content) { super(serial, serviceURL, target, original, content); this.app_id = app_id; this.app_key = app_key; } @Override public TranslateResult call() { TimeInterval interval = DateUtil.timer(); HttpResponse response = HttpUtil.createPost(serviceURL) .form("q", content) .form("from", original) .form("to", target) .form("appid", app_id) .form("salt", SALT) .form("sign", MD5.create().digestHex(app_id+ content + SALT + app_key)) .contentType(ContentType.FORM_URLENCODED.getValue()) .charset("UTF-8") .setFollowRedirects(true) .execute(); JSONObject resp = JSONUtil.parseObj(response.body()); TranslateResult result = TranslateResult.builder().serial(serial).build(); if (response.isOk() && !resp.containsKey("error_code")) { result.setSuccess(true); List dataList = resp.getJSONArray("trans_result").toList(JSONObject.class); result.setData(dataList.stream().map(e -> e.getStr("dst")).collect(Collectors.joining())); } else { result.setSuccess(false); result.setData(resp.getStr("error_msg")); } log.debug("序号:{} 请求 {},耗时:{} ms", serial,result.isSuccess()? "成功":"失败", interval.intervalMs()); // log.debug(resp.toStringPretty()); return result; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java ================================================ package org.fordes.subtitles.view.service.translate.thread; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.date.format.FastDateFormat; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.http.ContentType; import cn.hutool.http.Header; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.model.DTO.TranslateResult; import org.fordes.subtitles.view.utils.TranslateUtil; import java.net.URL; import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.stream.Collectors; /** * @author fordes on 2022/7/31 */ @Slf4j public class HuoShanTranslateThread extends TranslateThread implements Callable { private final String accessKeyId; private final String secretAccessKey; private final String versionDate; private final String region; static final String Action = "TranslateText"; static final String Service = "translate"; static final String Version = "1.0.16"; static final String Algorithm = "HMAC-SHA256"; public HuoShanTranslateThread(String versionDate, String region, String accessKeyId, String secretAccessKey, Integer serial, String serviceURL, String target, String original, String content) { super(serial, serviceURL, target, original, content); this.versionDate = versionDate; this.region = region; this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; } @Override public TranslateResult call() throws Exception { TimeInterval interval = DateUtil.timer(); //请求路径 URL url = URLUtil.url(serviceURL); //请求体 String body = new JSONObject() .putOnce("SourceLanguage", original) //原语言 .putOnce("TargetLanguage", target) //目标语言 .putOnce("TextList", CollUtil.newArrayList(content)).toString(); //待翻译文本列表,长度不大于128 String bodyHash = SecureUtil.sha256(body); //时间 (必须使用UTC时间) DateTime now = DateTime.now(); String nowDate = now.toString(FastDateFormat.getInstance(DatePattern.PURE_DATE_PATTERN, TimeZone.getTimeZone("UTC"))); String nowTime = now.toString(FastDateFormat.getInstance(DatePattern.PURE_TIME_PATTERN, TimeZone.getTimeZone("UTC"))); String requestDate = nowDate + "T" + nowTime + "Z"; //构造需要计入签名的部分请求头 Map signHeadMap = MapUtil.newHashMap(); signHeadMap.put(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue()); signHeadMap.put(Header.HOST.getValue(), url.getHost()); signHeadMap.put("X-Date", requestDate); signHeadMap.put("X-Content-Sha256", bodyHash); //按照ASCII也即字母序排序 TreeMap signHeadMapSort = MapUtil.sort(signHeadMap); // 正规化请求 String requestMethod = "POST"; String canonicalURI = "/"; String canonicalQueryString = StrUtil.format("Action={}&Version={}", Action, versionDate); StringBuilder canonicalHeaders = new StringBuilder(); signHeadMapSort.forEach((key, value) -> canonicalHeaders.append(key.trim().toLowerCase()) .append(StrUtil.COLON).append(value.trim()).append(StrUtil.LF)); String SignedHeaders = CollUtil.join(signHeadMapSort.keySet(), ";").trim().toLowerCase(); String canonicalRequest = StrUtil.concat(false, requestMethod, StrUtil.LF, canonicalURI, StrUtil.LF, canonicalQueryString, StrUtil.LF, canonicalHeaders, StrUtil.LF, SignedHeaders, StrUtil.LF, bodyHash); // 签名 String CredentialScope = StrUtil.concat(false, nowDate, StrUtil.SLASH, region, StrUtil.SLASH, Service, "/request"); String StringToSign = StrUtil.concat(false, Algorithm, StrUtil.LF, requestDate, StrUtil.LF, CredentialScope, StrUtil.LF, SecureUtil.sha256(canonicalRequest)); //计算签名密钥 byte[] kDate = TranslateUtil.hmac256(secretAccessKey, nowDate); byte[] kRegion = TranslateUtil.hmac256(kDate, region); byte[] kService = TranslateUtil.hmac256(kRegion, Service); byte[] kSigning = TranslateUtil.hmac256(kService, "request"); //计算签名 String Signature = HexUtil.encodeHexStr(TranslateUtil.hmac256(kSigning, StringToSign)); //拼接出授权头 String Authorization = StrUtil.format("{} Credential={}/{}, SignedHeaders={}, Signature={}", Algorithm, accessKeyId, CredentialScope, SignedHeaders, Signature); //创建真实请求 HttpResponse response = HttpUtil.createPost(serviceURL) .header(Header.CONTENT_TYPE, ContentType.JSON.getValue()) .header(Header.ACCEPT, ContentType.JSON.getValue()) .header(Header.HOST, url.getHost()) .header(Header.USER_AGENT, "volc-sdk-java/v" + Version) .header("X-Date", requestDate) .header("X-Content-Sha256", bodyHash) .header(Header.AUTHORIZATION, Authorization) .setFollowRedirects(true) .body(body) .execute(); //解析结果 JSONObject resp = JSONUtil.parseObj(response.body()); TranslateResult result = TranslateResult.builder().serial(serial).build(); if (response.isOk() && resp.containsKey("TranslationList")) { result.setSuccess(true); result.setData(resp.getJSONArray("TranslationList").stream() .map(e -> JSONUtil.parseObj(e).getStr("Translation")) .collect(Collectors.joining(StrUtil.LF))); } else { result.setSuccess(false); result.setData(resp.getJSONObject("ResponseMetadata") .getJSONObject("Error").getStr("Message")); } log.debug("序号:{} 请求 {},耗时:{} ms", serial, result.isSuccess() ? "成功" : "失败", interval.intervalMs()); // log.debug(resp.toStringPretty()); return result; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java ================================================ package org.fordes.subtitles.view.service.translate.thread; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.date.format.FastDateFormat; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.URLUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.model.DTO.TranslateResult; import org.fordes.subtitles.view.utils.TranslateUtil; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.TimeZone; import java.util.concurrent.Callable; /** * @author fordes on 2022/7/29 */ @Slf4j public class TencentTranslateThread extends TranslateThread implements Callable { static final String CT_JSON = "application/json; charset=utf-8"; static final String SERVICE = "tmt"; static final String ACTION = "TextTranslate"; static final String VERSION = "2018-03-21"; static final String ALGORITHM = "TC3-HMAC-SHA256"; private final String secretId; private final String secretKey; private final String region; public TencentTranslateThread(String secretId, String secretKey, String region, Integer serial, String serviceURL, String target, String original, String content) { super(serial, serviceURL, target, original, content); this.region = region; this.secretId = secretId; this.secretKey = secretKey; } @Override public TranslateResult call() throws Exception { TimeInterval interval = DateUtil.timer(); URL url = URLUtil.url(serviceURL); //时间 long now = DateUtil.currentSeconds(); String timestamp = String.valueOf(now); String date = DateTime.of(now * 1000) .toString(FastDateFormat.getInstance(DatePattern.NORM_DATE_PATTERN, TimeZone.getTimeZone("UTC"))); //拼接规范请求串 String httpRequestMethod = "POST"; String canonicalUri = "/"; String canonicalQueryString = ""; String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + url.getHost() + "\n"; String signedHeaders = "content-type;host"; //整合参数 Dict param = Dict.of( "SourceText", content, "Source", original, "Target", target, "ProjectId", 0); String payload = JSONUtil.toJsonStr(param); String hashedRequestPayload = TranslateUtil.sha256Hex(payload); String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload; //拼接待签名字符串 String credentialScope = date + "/" + SERVICE + "/" + "tc3_request"; String hashedCanonicalRequest = TranslateUtil.sha256Hex(canonicalRequest); String stringToSign = ALGORITHM + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; //计算签名 byte[] secretDate = TranslateUtil.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date); byte[] secretService = TranslateUtil.hmac256(secretDate, SERVICE); byte[] secretSigning = TranslateUtil.hmac256(secretService, "tc3_request"); String signature = HexUtil.encodeHexStr(TranslateUtil.hmac256(secretSigning, stringToSign)).toLowerCase(); //拼接 Authorization String authorization = ALGORITHM + " " + "Credential=" + secretId + "/" + credentialScope + ", " + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; HttpResponse response = HttpUtil.createPost(serviceURL) .header("Authorization", authorization) .header("Content-Type", CT_JSON) .header("Host", url.getHost()) .header("X-TC-Action", ACTION) .header("X-TC-Timestamp", timestamp) .header("X-TC-Version", VERSION) .header("X-TC-Region", region) .setFollowRedirects(true) .body(payload) .execute(); JSONObject resp = JSONUtil.parseObj(response.body()); TranslateResult result = TranslateResult.builder().serial(serial) .success(false).data("翻译失败!").build(); if (response.isOk() && resp.containsKey("Response")) { JSONObject respJson = resp.getJSONObject("Response"); if (respJson.containsKey("TargetText")) { result.setSuccess(true); result.setData(respJson.getStr("TargetText")); }else { result.setSuccess(false); result.setData(respJson.getJSONObject("Error").getStr("Message")); } } long intervalTime = interval.intervalMs(); log.debug("序号:{} 请求 {},耗时:{} ms", serial,result.isSuccess()? "成功":"失败", intervalTime); // log.debug(resp.toStringPretty()); return result; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java ================================================ package org.fordes.subtitles.view.service.translate.thread; import lombok.AllArgsConstructor; /** * 翻译线程抽象 * * @author fordes on 2022/7/27 */ @AllArgsConstructor public abstract class TranslateThread { /** * 序号,将随结果返回,用于还原顺序 */ public Integer serial; /** * 服务地址,即api调用地址 */ public String serviceURL; /** * 目标语言 通常为代码 */ public String target; /** * 原语言 通常为代码 */ public String original; /** * 待翻译内容 */ public String content; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java ================================================ package org.fordes.subtitles.view.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; import lombok.extern.slf4j.Slf4j; import net.sf.sevenzipjbinding.ExtractOperationResult; import net.sf.sevenzipjbinding.IInArchive; import net.sf.sevenzipjbinding.SevenZip; import net.sf.sevenzipjbinding.SevenZipException; import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; import net.sf.sevenzipjbinding.simple.ISimpleInArchive; import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem; import org.fordes.subtitles.view.enums.FileEnum; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.charset.Charset; import java.util.Collection; import java.util.Collections; /** * 文件解压工具类 * * @author fordes on 2022/4/4 */ @Slf4j public class ArchiveUtil { /** * 解压文件至当前路径下uuid命名路径 并删除原文件 * 将排除不受支持的文件 * * @param file 压缩文件 * @return 文件路径 */ public static Collection unArchiveToCurrentPath(File file) { Collection result = Collections.emptyList(); if (FileUtil.exist(file)) { String outPath = StrUtil.concat(false, file.getParent(), File.separator, UUID.fastUUID().toString()); //创建目标文件夹 if (!FileUtil.exist(outPath)) { FileUtil.mkdir(outPath); } String suffix = FileUtil.getSuffix(file); if (StrUtil.equalsAnyIgnoreCase(suffix, FileEnum.SUPPORT_SUBTITLE)) { File newFile = FileUtil.file(StrUtil.concat(false, outPath, File.separator, URLUtil.decode(file.getName(), Charset.defaultCharset()))); FileUtil.move(file, newFile, true); result = CollUtil.newArrayList(newFile); } else { result = unArchiveFile(file, outPath, FileEnum.SUPPORT_SUBTITLE); } FileUtil.del(file); } return result; } /** * 解压文件,不保留内部结构 * * @param in 压缩文件路径 * @param outPath 输出路径 * @param filter 指定需要提取的文件后缀 如 ass */ public static Collection unArchiveFile(File in, String outPath, String... filter) { Collection result = CollUtil.newArrayList(); TimeInterval interval = DateUtil.timer(); RandomAccessFile randomAccessFile = null; IInArchive inArchive = null; try { randomAccessFile = new RandomAccessFile(in.getPath(), "r"); inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile)); ISimpleInArchive archive = inArchive.getSimpleInterface(); for (ISimpleInArchiveItem item : archive.getArchiveItems()) { if (!item.isFolder() && StrUtil.equalsAnyIgnoreCase(FileUtil.getSuffix(item.getPath()), filter)) { File file = FileUtil.file(StrUtil.concat(false, outPath, File.separator, item.getPath())); ExtractOperationResult operationResult = item.extractSlow(data -> { FileUtil.writeBytes(data, file); return data.length; }); if (operationResult == ExtractOperationResult.OK) { result.add(file); log.debug("提取成功 => {}", item.getPath()); } else { log.error("提取失败 => {}\n{}", item.getPath(), operationResult); } } } } catch (FileNotFoundException | SevenZipException e) { log.error("解压文件出错!{} => {}", in.getPath(), outPath); log.error(ExceptionUtil.stacktraceToString(e)); } finally { if (inArchive != null) { try { inArchive.close(); } catch (SevenZipException e) { log.error(ExceptionUtil.stacktraceToString(e)); } } if (randomAccessFile != null) { try { randomAccessFile.close(); } catch (IOException e) { log.error(ExceptionUtil.stacktraceToString(e)); } } if (result.isEmpty()) { FileUtil.del(outPath); } } log.debug("解压文件:{} => {},耗时:{} ms", in.getPath(), outPath, interval.intervalMs()); return result; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java ================================================ package org.fordes.subtitles.view.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Dict; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import org.fordes.subtitles.view.enums.ServiceProvider; import org.fordes.subtitles.view.enums.ServiceType; import org.fordes.subtitles.view.mapper.InterfaceMapper; import org.fordes.subtitles.view.model.PO.Language; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * 缓存 * * @author fordes on 2022/7/27 */ public class CacheUtil { public static final Map>> languageMap = MapUtil.newHashMap(); /** * 初始化语言字典 * * @param data 数据 */ public static void initLanguageDict(List data) { data.stream().collect(Collectors.groupingBy(e -> ServiceType.valueOf(e.getStr(Language.COL_TYPE)))) .forEach((k, v) -> { Map> providerMap = MapUtil.newHashMap(); Arrays.stream(ServiceProvider.values()).forEach(p -> { Map> idMap = v.stream() .filter(q -> q.containsKey(p.name().toLowerCase())) .map(q -> { String target = MapUtil.getStr(q, p.name().toLowerCase() + Language.TARGET); return new Language() .setId(q.getInt(Language.COL_ID)) .setName(q.getStr(Language.COL_NAME)) .setCode(q.getStr(p.name().toLowerCase())) .setGeneral(q.getBool(Language.COL_GENERAL)) .set_target(StrUtil.split(target, StrUtil.COMMA, true, true)); }) .collect(Collectors.groupingBy(Language::getCode, Collectors.toList())); List languageList = CollUtil.newArrayList(); idMap.forEach((x, y) -> { Language item = y.get(0); if (item.get_target().isEmpty()) { item.setTarget(idMap.values().stream().map(e -> e.get(0)).collect(Collectors.toList())); } else { item.setTarget(item.get_target().stream() .map(e -> idMap.get(e).get(0)) .collect(Collectors.toList())); } languageList.add(item); }); providerMap.put(p, languageList); }); languageMap.put(k, providerMap); }); } /** * 获取语言字典 * * @param type {@link ServiceType} * @param provider {@link ServiceProvider} * @param general 是否只获取常用语言 * @return {@link List} */ public static List getLanguageDict(ServiceType type, ServiceProvider provider, boolean general) { if (languageMap.isEmpty()) { CacheUtil.initLanguageDict(SpringUtil.getBean(InterfaceMapper.class).getLanguageList()); } List result = languageMap.get(type).get(provider); return general ? result.stream().filter(Language::isGeneral).collect(Collectors.toList()) : result; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/FileUtils.java ================================================ package org.fordes.subtitles.view.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.resource.ClassPathResource; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.constant.CommonConstant; import org.fordes.subtitles.view.enums.FileEnum; import org.fordes.subtitles.view.model.DTO.Subtitle; import org.fordes.subtitles.view.model.DTO.Video; import org.fordes.subtitles.view.model.PO.FileRecord; import org.fordes.subtitles.view.utils.submerge.utils.EncodeUtils; import org.springframework.lang.NonNull; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Arrays; import java.util.Collection; import java.util.stream.Collectors; import static cn.hutool.core.thread.ThreadUtil.sleep; /** * 文件工具类 * * @author fordes on 2022/1/23 */ @Slf4j public class FileUtils { /** * 根据路径获取文件流,支持http和resource * @param path * @return */ public static InputStream getStream(@NonNull String path) { if (ReUtil.isMatch("^http[s]?://.*", path)) { HttpResponse response = HttpUtil.createGet(path, true).execute(); if (response.isOk()) { return response.bodyStream(); } }else { ClassPathResource resource = new ClassPathResource(path); return resource.getStream(); } throw new RuntimeException(StrUtil.format("resource: {} not found", path)); } /** * 选择文件 * @param title 选择框标题内容 * @param items 选项 * @return 返回指定文件选择器 */ public static FileChooser chooseFile(String title, FileEnum... items) { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(title); fileChooser.setInitialDirectory(new File(CommonConstant.PATH_HOME)); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("全部文件", "*.*")); if (ArrayUtil.isNotEmpty(items)) { fileChooser.getExtensionFilters().addAll(Arrays.stream(items) .filter(e -> e.support) .map(e -> new FileChooser.ExtensionFilter(e.suffix, CommonConstant.PREFIX + e.suffix)) .collect(Collectors.toList())); } return fileChooser; } /** * 选择路径 * @return 文件夹选择器 */ public static DirectoryChooser choosePath(String path) { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle(CommonConstant.TITLE_PATH); directoryChooser.setInitialDirectory(FileUtil.file(StrUtil.isNotEmpty(path)? path: CommonConstant.PATH_HOME)); return directoryChooser; } /** * 读取文件信息 * @param file 文件 * @return 文件信息实例 */ public static FileRecord readFileInfo(File file) throws IOException { String suffix = FileUtil.extName(file); FileRecord info; FileEnum type = FileEnum.of(FileUtil.getSuffix(file)); assert type != null; if (type.media) { info = new Video().setFormat(type); }else { info = new Subtitle().setCharset(EncodeUtils.guessEncoding(file)).setFormat(type); } return info.setFile(file) .setFile_name(file.getName()) .setPath(file.getPath()) .setSize(FileUtil.readableFileSize(file)) .setFile_modify_time(FileUtil.lastModifiedTime(file)); } /** * 加锁将集合按行写入文件 * * @param file 目标文件 * @param content 内容集合 */ public static void write(File file, Collection content, String charset) { write(file,CollUtil.join(content, StrUtil.CRLF), charset); } public static void write(File file, String content, String charset) { if (StrUtil.isNotEmpty(content)) { try (RandomAccessFile accessFile = new RandomAccessFile(file, "rw"); FileChannel channel = accessFile.getChannel()) { //加锁写入文件,如获取不到锁则休眠 FileLock fileLock = null; while (true) { try { fileLock = channel.tryLock(); break; } catch (Exception e) { sleep(1000); } } accessFile.seek(accessFile.length()); accessFile.write(content.getBytes(charset)); accessFile.write(StrUtil.CRLF.getBytes(charset)); } catch (IOException ioException) { log.error("写入文件出错,{} => {}", file.getPath(), ioException.getMessage()); throw new RuntimeException("写入文件出错"); } } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java ================================================ package org.fordes.subtitles.view.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.lang.Singleton; import cn.hutool.core.util.StrUtil; import javafx.scene.control.IndexRange; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.enums.FileEnum; import org.fordes.subtitles.view.handler.CallBackHandler; import org.fordes.subtitles.view.model.DTO.Subtitle; import org.fordes.subtitles.view.utils.submerge.parser.ParserFactory; import org.fordes.subtitles.view.utils.submerge.parser.SubtitleParser; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import org.fxmisc.richtext.StyleClassedTextArea; import java.time.LocalTime; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author fordes on 2022/7/19 */ @Slf4j public class SubtitleUtil { /** * 纯文本搜索 使用 {@link SearchCache} 单例作为缓存 * * @see SubtitleUtil#search(SearchCache, StyleClassedTextArea, String, boolean, boolean) */ public static void search(StyleClassedTextArea area, String target, boolean isIgnoreCase, boolean isRegular) { search(Singleton.get(SearchCache.class), area, target, isIgnoreCase, isRegular); } /** * 文本替换 前置 搜索 使用 {@link ReplaceCache} 单例作为缓存 * * @see SubtitleUtil#search(SearchCache, StyleClassedTextArea, String, boolean, boolean) (ReplaceCache, StyleClassedTextArea, String, String, boolean, boolean) */ public static void find(StyleClassedTextArea area, String target, boolean isIgnoreCase, boolean isRegular) { search(Singleton.get(ReplaceCache.class), area, target, isIgnoreCase, isRegular); } /** * 简易文本搜索 * * @param area 被搜索文本 * @param target 目标关键字 * @param isIgnoreCase 忽略大小写 * @param isRegular 正则搜索 */ public static void search(T cache, StyleClassedTextArea area, String target, boolean isIgnoreCase, boolean isRegular) { int cursor; String text; if (StrUtil.equals(cache.getTarget(), target)) { cursor = cache.getAnchor() + 1; text = area.getText(cursor, area.getLength()); } else { cache.reset(); cursor = 0; text = area.getText(); } int start = 0, end = 0; for (String line : text.split(StrUtil.LF)) { if (isRegular) { Matcher matcher = Pattern.compile(target).matcher(line); if (matcher.find()) { start = cursor + line.indexOf(matcher.group(0)); end = cursor + line.indexOf(matcher.group(0)) + matcher.group(0).length(); break; } } else { int pos = StrUtil.indexOf(line, target, 0, isIgnoreCase); if (pos >= 0) { start = cursor + pos; end = cursor + pos + target.length(); break; } } cursor += line.length() + 1; } if (start != 0 && end != 0) { area.moveTo(end); area.requestFollowCaret(); area.selectRange(start, end); cache.setAnchor(start); cache.setTarget(target); if (cache instanceof ReplaceCache) { ((ReplaceCache) cache).setCaretPosition(end); } } else cache.reset(); } /** * 文本替换 * * @param area 被处理文本区 * @param subtitle 对应字幕文件 * @param searchStr 被替换内容 * @param replaceStr 替换内容 * @param isAll 是否替换全部 * @param isIgnoreCase 是否忽略大小写 * @param isRegular (searchStr)是否为正则表达式 */ public static void replace(StyleClassedTextArea area, Subtitle subtitle, String searchStr, String replaceStr, boolean isAll, boolean isIgnoreCase, boolean isRegular) throws Exception { if (isAll) { String text = area.getText(); if (isRegular) { Matcher matcher = Pattern.compile(searchStr).matcher(text); if (matcher.find()) { text = matcher.replaceAll(replaceStr); } } else { text = isIgnoreCase ? StrUtil.replaceIgnoreCase(text, searchStr, replaceStr) : StrUtil.replace(text, searchStr, replaceStr); } area.clear(); area.append(text, StrUtil.EMPTY); } else { ReplaceCache cache = Singleton.get(ReplaceCache.class); if (StrUtil.equals(searchStr, area.getText(cache.getAnchor(), cache.getCaretPosition()))) { area.replace(cache.getAnchor(), cache.getCaretPosition(), replaceStr, StrUtil.EMPTY); } else { search(cache, area, searchStr, isIgnoreCase, isRegular); if (StrUtil.equals(searchStr, area.getText(cache.getAnchor(), cache.getCaretPosition()))) { area.replace(cache.getAnchor(), cache.getCaretPosition(), replaceStr, StrUtil.EMPTY); }else return; } } TimedTextFile timedTextFile = SubtitleUtil.parse(area.getText(), subtitle.getFormat()); subtitle.setTimedTextFile(timedTextFile); } /** * 时间轴位移 * * @param timedTextFile 字幕 * @param begin 开始时间 * @param range 位移范围 * @param mode 显示模式 * @return 时间轴位移后的字幕 */ public static TimedTextFile revise(TimedTextFile timedTextFile, LocalTime begin, IndexRange range, boolean mode) { LocalTime start = CollUtil.getFirst(timedTextFile.getTimedLines()).getTime().getStart(); long poor = begin.toNanoOfDay() - start.toNanoOfDay(); if (range != null) { long sort = 0; for (TimedLine item : timedTextFile.getTimedLines()) { sort += toStr(item, mode).length(); if (sort > range.getEnd()) { break; } else if (sort >= range.getStart()) { item.getTime().setStart(LocalTime.ofNanoOfDay(item.getTime().getStart().toNanoOfDay() + poor)); item.getTime().setEnd(LocalTime.ofNanoOfDay(item.getTime().getEnd().toNanoOfDay() + poor)); } } } else { for (TimedLine item : timedTextFile.getTimedLines()) { revise(item.getTime(), poor); } } return timedTextFile; } /** * @see #revise(TimedTextFile, LocalTime, IndexRange, boolean) */ private static void revise(TimedObject timedLine, long poor) { timedLine.setStart(LocalTime.ofNanoOfDay(timedLine.getStart().toNanoOfDay() + poor)); timedLine.setEnd(LocalTime.ofNanoOfDay(timedLine.getEnd().toNanoOfDay() + poor)); } /** * @see #revise(TimedTextFile, LocalTime, IndexRange, boolean) */ public static TimedTextFile revise(TimedTextFile timedTextFile, LocalTime begin, boolean mode) { return revise(timedTextFile, begin, null, mode); } /** * 从文件解析字幕 * * @param subtitle 字幕文件 * @throws Exception 异常 */ public static void parse(Subtitle subtitle) throws Exception { TimeInterval timer = DateUtil.timer(); SubtitleParser parser = ParserFactory.getParser(subtitle.getFormat().suffix); TimedTextFile content = parser.parse(subtitle.getFile(), subtitle.getCharset()); log.debug("解析字幕耗时:{} ms", timer.interval()); subtitle.setTimedTextFile(content); } /** * 从文本字幕解析字幕 * * @param str 字幕文本 * @param type 字幕格式 * @return 字幕结构 * @throws Exception 异常 */ public static TimedTextFile parse(String str, FileEnum type) throws Exception { return ParserFactory.getParser(type.suffix).parse(str, StrUtil.EMPTY); } /** * 字幕结构转换为字符串 * * @param mode 解析模式 f-简洁模式 t-完整模式 * @return 字符串 */ public static String toStr(TimedTextFile subtitle, boolean mode) { if (!mode) { StringBuilder content = new StringBuilder(); subtitle.getTimedLines().forEach(item -> content.append(CollUtil.join(item.getTextLines(), StrUtil.CRLF)).append(StrUtil.CRLF)); return content.toString(); } else { return subtitle.toString(); } } /** * 字幕结构转换为字符串 * * @param mode 解析模式 f-简洁模式 t-完整模式 * @return 字符串 */ public static String toStr(TimedLine timedLine, boolean mode) { return mode ? timedLine.toString() : CollUtil.join(timedLine.getTextLines(), StrUtil.CRLF); } /** * 写入字幕结构到源文件 * * @param subtitle 字幕 * @param handler 回调 */ public static void write(Subtitle subtitle, CallBackHandler handler) { try { FileUtils.write(subtitle.getFile(), subtitle.getTimedTextFile().toString(), subtitle.getCharset()); } catch (RuntimeException e) { handler.handle(false); } handler.handle(true); } /** * 搜索操作 上一步结果缓存 */ @Data static class SearchCache { private String target; private int anchor; public SearchCache() { reset(); } public void reset() { this.target = StrUtil.EMPTY; this.anchor = 0; } } /** * 替换操作 上一步结果缓存 */ @Data @EqualsAndHashCode(callSuper = false) static class ReplaceCache extends SearchCache { private int caretPosition; @Override public void reset() { this.caretPosition = 0; super.reset(); } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java ================================================ package org.fordes.subtitles.view.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.model.DTO.Subtitle; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.StringReader; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; /** * 翻译工具 * * @author fordes on 2022/7/26 */ @Slf4j public class TranslateUtil { private final static String QUESTION_MARK = "?"; public final static String SEPARATIST = "><"; /** * 字符串切分,按照指定的分隔符切分字符串为长度不超过{@code maxLength}的集合 * @param content 内容 * @param maxLength 最大长度 * @return 切分后的集合 */ public static List segmented(String content, int maxLength) { List result = CollUtil.newArrayList(); try (StringReader reader = StrUtil.getReader(content)) { StringBuilder builder = StrUtil.builder(); StringBuilder temp = StrUtil.builder(); CharBuffer buffer = CharBuffer.allocate(1); while (-1 != reader.read(buffer)) { CharSequence s = buffer.flip(); temp.append(s); if (StrUtil.equalsAny(s, StrUtil.COMMA, StrUtil.DOT, StrUtil.LF, QUESTION_MARK)) { if (builder.length() + temp.length() >= maxLength) { result.add(builder.toString()); builder.setLength(0); } builder.append(temp); temp.setLength(0); } } result.add(builder.toString()); } catch (IOException e) { throw new IORuntimeException(e); } return result; } /** * @see #segmented(String, int) * 重载方法 多行文本使用\n分割 */ public static List segmented(List content, int maxLength) { return segmented(CollUtil.join(content, StrUtil.LF), maxLength); } /** * @see #segmented(String, int) * 重载方法 每段字幕使用 {@link #SEPARATIST} 分割 */ public static List segmented(Subtitle subtitle, int maxLength) { List data = CollUtil.newArrayList(); subtitle.getTimedTextFile().getTimedLines().forEach(e -> data.add(CollUtil.join(e.getTextLines(), SEPARATIST))); return segmented(data, maxLength); } /** * 将经过 {@link #segmented(Subtitle, int)} 切分后的结构还原至字幕文件中 * @param subtitle 字幕 * @param data 数据 * @param mode 模式 f-覆盖模式,t-追加模式 */ public static void reduction(Subtitle subtitle, List data, boolean mode) { StringBuilder builder = StrUtil.builder(); data.forEach(builder::append); List lines = StrUtil.split(builder.toString(), StrUtil.LF); for (int part = 0; part < lines.size(); part++) { TimedLine line = CollUtil.get(subtitle.getTimedTextFile().getTimedLines(), part); List second = StrUtil.split(lines.get(part), SEPARATIST); if (mode) { List temp = new ArrayList<>(line.getTextLines().size()); List first = line.getTextLines(); for (int i = 0; i < first.size(); i++) { temp.add(CollUtil.get(first, i)); temp.add(CollUtil.get(second, i)); } line.setTextLines(temp); }else { line.setTextLines(second); } } } public static byte[] hmac256(byte[] key, String msg) throws Exception { return hmac256(key, msg.getBytes(StandardCharsets.UTF_8)); } public static byte[] hmac256(byte[] key, byte[] msg) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm()); mac.init(secretKeySpec); return mac.doFinal(msg); } public static byte[] hmac256(String key, String msg) throws Exception { return hmac256(key.getBytes(StandardCharsets.UTF_8), msg.getBytes(StandardCharsets.UTF_8)); } public static String sha256Hex(String s) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] d = md.digest(s.getBytes(StandardCharsets.UTF_8)); return HexUtil.encodeHexStr(d).toLowerCase(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java ================================================ package org.fordes.subtitles.view.utils.search; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import org.fordes.subtitles.view.model.search.Selector; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * HTML解析器 * * @author fordes on 2022/3/29 */ public class HTMLParsing extends Parsing { private Document doc; public HTMLParsing(Object data) { super(data); this.doc = Jsoup.parse((String) data); } @Override public Object parsing(Selector selector) { List fields = getFields(doc, selector); return selector.only ? CollUtil.getFirst(fields): fields; } private static List getFields(Document doc, Selector selector) { if (ObjectUtil.isNotEmpty(selector)) { return doc.select(selector.css).stream() .map(e -> getField(e, selector.attr, selector.regular, selector.format)) .collect(Collectors.toList()); }else { return Collections.emptyList(); } } private static String getField(Element element, String attr, String regular, String format) { String attrField = StrUtil.isBlank(attr) ? element.text() : element.attr(attr); String regField = StrUtil.isBlank(regular) ? StrUtil.trim(attrField) : CollUtil.join(ReUtil.findAll(regular, attrField, 1), StrUtil.EMPTY); return StrUtil.isBlank(format) ? regField : StrUtil.format(format, regField); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java ================================================ package org.fordes.subtitles.view.utils.search; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import org.fordes.subtitles.view.model.search.Selector; import java.util.List; /** * JSON解析器 * * @author fordes on 2022/3/29 */ public class JSONParsing extends Parsing { private JSONObject json; public JSONParsing(Object data) { super(data); this.json = JSONUtil.parseObj(data); } //jsonKey > regular > foramt //TODO 未测试 @Override public Object parsing(Selector selector) { List keys = StrUtil.split(selector.jsonKey, StrUtil.C_DOT); for (int i = 0; i < keys.size(); i++) { if (i == keys.size()-1) { return json.get(keys.get(i)); }else { json = json.getJSONObject(keys.get(i)); } } return json; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java ================================================ package org.fordes.subtitles.view.utils.search; import org.fordes.subtitles.view.model.search.Selector; /** * 解析器抽象 * * @author fordes on 2022/3/29 */ public abstract class Parsing { public Parsing(Object data) { } public abstract Object parsing(Selector selector); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/search/ParsingFactory.java ================================================ package org.fordes.subtitles.view.utils.search; import cn.hutool.http.ContentType; import org.fordes.subtitles.view.model.search.Selector; /** * 解析器工厂 * * @author fordes on 2022/3/29 */ public class ParsingFactory { private Parsing parsing; public ParsingFactory(Object data, ContentType contentType) { parsing = ContentType.JSON.equals(contentType)? new JSONParsing(data): new HTMLParsing(data); } public Object getResult(Selector selector) { return parsing.parsing(selector); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/SubmergeAPI.java ================================================ package org.fordes.subtitles.view.utils.submerge; import cn.hutool.core.util.StrUtil; import org.fordes.subtitles.view.utils.submerge.subtitle.ass.ASSSub; import org.fordes.subtitles.view.utils.submerge.subtitle.ass.Events; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import org.fordes.subtitles.view.utils.submerge.subtitle.config.SimpleSubConfig; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTLine; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTSub; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime; import org.fordes.subtitles.view.utils.submerge.utils.ConvertUtils; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Service used to manage subtitles */ public class SubmergeAPI { /** * Change the framerate of a subtitle * * @param timedFile the subtitle * @param sourceFramerate le source framerate. Ex: 25.000 * @param targetFramerate the target framerate. Ex: 23.976 */ public void convertFramerate(TimedTextFile timedFile, double sourceFramerate, double targetFramerate) { double ratio = sourceFramerate / targetFramerate; timedFile.getTimedLines().forEach(timedLine -> { TimedObject time = timedLine.getTime(); long s = Math.round(time.getStart().toNanoOfDay() * ratio); long e = Math.round(time.getEnd().toNanoOfDay() * ratio); time.setStart(LocalTime.ofNanoOfDay(s)); time.setEnd(LocalTime.ofNanoOfDay(e)); }); } /** * TimedTextFile to SRT conversion * * @param timedFile the TimedTextFile * @return the SRTSub object */ public SRTSub toSRT(TimedTextFile timedFile) { SRTSub srt = new SRTSub(); int i = 0; for (TimedLine timedLine : timedFile.getTimedLines()) { int id = ++i; TimedObject time = timedLine.getTime(); SRTTime srtTime = new SRTTime(time.getStart(), time.getEnd()); List textLines = timedLine.getTextLines(); List newLines = new ArrayList<>(); for (String textLine : textLines) { newLines.add(ConvertUtils.toSRTString(textLine)); } SRTLine srtLine = new SRTLine(id, srtTime, newLines); srt.add(srtLine); } return srt; } /** * SubInput to ASS conversion * * @param config the configuration object * @return the ASSSub object */ public ASSSub toASS(SimpleSubConfig config) { return mergeToAss(config); } /** * Merge several subtitles into one ASS * * @param configs : configuration object of the subtitles * @return */ public ASSSub mergeToAss(SimpleSubConfig... configs) { ASSSub ass = new ASSSub(); Set ev = ass.getEvents(); for (SimpleSubConfig config : configs) { ass.getStyle().add(ConvertUtils.createV4Style(config)); TimedTextFile sub = config.getSub(); sub.getTimedLines().forEach(line -> ev.add(ConvertUtils.createEvent(line, config.getStyleName()))); } return ass; } /** * Transform all multi-lines subtitles to single-line * * @param timedFile the TimedTextFile */ public void mergeTextLines(TimedTextFile timedFile) { timedFile.getTimedLines().forEach(item -> { List textLines = item.getTextLines(); if (textLines.size() > 1) { textLines.set(0, String.join(StrUtil.SPACE, textLines)); textLines.subList(1, textLines.size()).clear(); } }); } /** * Synchronise the timecodes of a subtitle from another one * * @param fileToAdjust the subtitle to modify * @param referenceFile the subtitle to take the timecodes from * @param delay the number of milliseconds allowed to differ */ public void adjustTimecodes(TimedTextFile fileToAdjust, TimedTextFile referenceFile, int delay) { TimedLinesAPI linesAPI = new TimedLinesAPI(); List timedLines = new ArrayList<>(fileToAdjust.getTimedLines()); List referenceLines = new ArrayList<>(referenceFile.getTimedLines()); for (TimedLine lineToAdjust : timedLines) { TimedObject originalTime = lineToAdjust.getTime(); LocalTime originalStart = originalTime.getStart(); TimedLine referenceLine = linesAPI.closestByStart(referenceLines, originalStart, delay); if (referenceLine != null) { LocalTime targetStart = referenceLine.getTime().getStart(); LocalTime targetEnd = referenceLine.getTime().getEnd(); TimedLine fullIntersect = linesAPI.intersected(timedLines, targetStart, targetEnd); if (fullIntersect != null && !lineToAdjust.equals(fullIntersect)) { continue; } TimedLine startIntersect = linesAPI.intersected(timedLines, targetStart); TimedLine endIntersect = linesAPI.intersected(timedLines, targetEnd); if (startIntersect == null || originalTime.equals(startIntersect.getTime())) { originalTime.setStart(targetStart); } else { originalTime.setStart(startIntersect.getTime().getEnd()); } if (endIntersect == null || originalTime.getStart().equals(endIntersect.getTime().getStart())) { originalTime.setEnd(targetEnd); } else { originalTime.setEnd(endIntersect.getTime().getStart()); } } } expandLongLines(timedLines, referenceLines, 1500); } /** * Expand lines in the adjusted file that should be displayed during 2 lines of the * reference file * * @param adjustedLines the adjusted lines (ascending sort) * @param referenceLines the reference lines (ascending sort) */ private static void expandLongLines(List adjustedLines, List referenceLines, int delay) { TimedLinesAPI linesAPI = new TimedLinesAPI(); for (int i = 0; i < adjustedLines.size(); i++) { TimedObject currentElement = adjustedLines.get(i).getTime(); int index = linesAPI.findByTime(referenceLines, currentElement); if (index >= 0) { int nextReferenceIndex = index + 1; if (nextReferenceIndex < referenceLines.size() && i + 1 < adjustedLines.size()) { TimedObject nextReference = referenceLines.get(nextReferenceIndex).getTime(); TimedObject nextElement = adjustedLines.get(i + 1).getTime(); if (linesAPI.isEqualsOrAfter(currentElement, nextReference) && linesAPI.getDelay(currentElement.getEnd(), nextReference.getStart()) < delay && linesAPI.isEqualsOrAfter(nextReference, nextElement)) { currentElement.setEnd(nextReference.getEnd()); } } } } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/TimedLinesAPI.java ================================================ package org.fordes.subtitles.view.utils.submerge; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.*; public class TimedLinesAPI { /** * Search the line that has the closest start time compared to a specified time. If * the gap beetween the two start times is greater than the toleranceDelay (in ms) the * line will be ignored. * * @param tolerance the maximum gap in millis * @param lines the lines (ascending sort) * @param time the target start time * @return */ public TimedLine closestByStart(List lines, final LocalTime time, int tolerance) { // Binary search will find the first "random" match int iAnyMatch = Collections.binarySearch(lines, new SubtitleLine<>(new SubtitleTime(time, null)), (compare, base) -> { LocalTime search = base.getTime().getStart(); LocalTime start = compare.getTime().getStart(); if (getDelay(search, start) < tolerance) { return 0; } return start.compareTo(search); }); if (iAnyMatch < 0) { return null; } // Search for other matches Set matches = new TreeSet<>(); matches.add(lines.get(iAnyMatch)); int i = iAnyMatch; while (i > 0) { TimedLine previous = lines.get(--i); if (getDelay(time, previous.getTime().getStart()) >= tolerance) { break; } matches.add(previous); } i = iAnyMatch; while (i < lines.size() -1) { TimedLine next = lines.get(++i); if (getDelay(time, next.getTime().getStart()) >= tolerance) { break; } matches.add(next); } // return the closest match return matches.stream().min((m1, m2) -> getDelay(m1.getTime().getStart(), time) - getDelay(m2.getTime().getStart(), time)).get(); } /** * Get the absolute delay beetween 2 times * * @return the absolute delay beetween 2 times */ public int getDelay(LocalTime start, LocalTime end) { return (int) Math.abs(ChronoUnit.MILLIS.between(start, end)); } /** * Check if a timed object appear before or at the same time as an other timed object * * @param elementToCompare * @param comparedElement * @return */ public boolean isEqualsOrAfter(TimedObject elementToCompare, TimedObject comparedElement) { return comparedElement.getStart().isAfter(elementToCompare.getEnd()) || comparedElement.getStart().equals(elementToCompare.getEnd()); } /** * Find the line displayed at targetTime * * @param lines the lines (ascending sort) * @param time the target time * @return */ public TimedLine intersected(List lines, LocalTime time) { int index = Collections.binarySearch(lines, new SubtitleLine<>(new SubtitleTime(time, null)), (compare, base) -> { LocalTime search = base.getTime().getStart(); LocalTime start = compare.getTime().getStart(); LocalTime end = compare.getTime().getEnd(); if ((start.isBefore(search) || start.equals(search)) && (end.isAfter(search) || start.equals(search))) { return 0; } return start.compareTo(search); }); return index < 0 ? null : lines.get(index); } /** * Find a line displayed between 2 times * * @param lines the lines (ascending sort) * @param * * @return */ public TimedLine intersected(List lines, LocalTime start, LocalTime end) { int index = Collections.binarySearch(lines, new SubtitleLine<>(new SubtitleTime(start, end)), (compare, base) -> { LocalTime searchStart = base.getTime().getStart(); LocalTime searchEnd = base.getTime().getEnd(); LocalTime start1 = compare.getTime().getStart(); LocalTime end1 = compare.getTime().getEnd(); if (searchStart.isBefore(start1) && searchEnd.isAfter(end1)) { return 0; } return compare.compareTo(base); }); return index < 0 ? null : lines.get(index); } /** * Find a sublitle line from it's time * * @param lines the subtitle lines * @param time the timed object * @return */ public int findByTime(List lines, TimedObject time) { return Collections.binarySearch(lines, new SubtitleLine<>(time), SubtitleLine.timeComparator); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/constant/FontName.java ================================================ package org.fordes.subtitles.view.utils.submerge.constant; /** * Enum all the supported font names of the application * */ public enum FontName { Arial("Arial"), CourierNew("Courier New"), Times("Times"), Helvetica("Helvetica"), DroidSans("Droid Sans"), Cursive("cursive"), Monospace("monospace"), Serif("serif"), SansSerif("sans-serif"), Fantasy("fantasy"), Courier("Courier"), Georgia("Georgia"), LucidaConsole("Lucida Console"), Papyrus("Papyrus"), Tahoma("Tahoma"), TeX("TeX"), Verdana("Verdana"), Verona("Verona"), SimSun("SimSun"), Ubuntu("Ubuntu"), UbuntuMono("Ubuntu Mono"), FreeMono("FreeMono"), LiberationSerif("Liberation Serif"), Purisa("Purisa"), TimesNewRoman("Times New Roman"); private String name; FontName(String name) { this.name = name; } /** * @return the name */ public String getName() { return this.name; } @Override public String toString() { return this.name; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ASSParser.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidAssSubException; import org.fordes.subtitles.view.utils.submerge.subtitle.ass.*; import org.fordes.subtitles.view.utils.submerge.utils.ColorUtils; import org.springframework.util.StringUtils; import java.beans.PropertyDescriptor; import java.io.BufferedReader; import java.io.IOException; import java.time.DateTimeException; import java.util.*; /** * Parse SSA/ASS subtitles */ public class ASSParser extends BaseParser { /** * Comments: lines that start with this character are ignored */ private static final String COMMENTS_MARK = ";"; @Override protected void parse(BufferedReader br, ASSSub sub) throws IOException, InvalidAssSubException { String line = readFirstTextLine(br); if (line != null && !StrUtil.equalsAnyIgnoreCase("[script info]", StrUtil.trim(line))) { throw new InvalidAssSubException("The line that says “[Script Info]” must be the first line in the script."); } // [Script Info] sub.setScriptInfo(parseScriptInfo(br)); while ((line = readFirstTextLine(br)) != null) { if (line.matches("(?i:^\\[v.*styles\\+?]$)")) { // [V4+ Styles] sub.setStyle(parseStyle(br)); } else if (line.equalsIgnoreCase("[events]")) { // [Events] sub.setEvents(parseEvents(br)); } } if (sub.getStyle().isEmpty()) { throw new InvalidAssSubException("Missing style definition"); } if (sub.getEvents().isEmpty()) { throw new InvalidAssSubException("No text line found"); } } /** * Parse the events section from the reader.
*

* Example of events section: * *

     * [Events]
     * Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
     * Dialogue: 0,0:02:30.84,0:02:34.70,StlyeOne,,0000,0000,0000,,A text line
     * Dialogue: 0,0:02:34.92,0:02:37.54,StyleTwo,,0000,0000,0000,,Another text line
     * 
* * @param br: the buffered reader * @throws IOException * @throws InvalidAssSubException * @throws IOException */ private static Set parseEvents(BufferedReader br) throws IOException, InvalidAssSubException { String[] eventsFormat = findFormat(br, "events"); Set events = new TreeSet<>(); String line = readFirstTextLine(br); while (line != null && !StrUtil.startWith(line, StrUtil.C_BRACKET_START)) { if (StrUtil.startWith(line, Events.DIALOGUE)) { String info = findInfo(line, Events.DIALOGUE); String[] dialogLine = StrUtil.splitToArray(info, Events.SEP); //StringUtils.splitByWholeSeparatorPreserveAllTokens(info, Events.SEP); int lengthDialog = dialogLine.length; int lengthFormat = eventsFormat.length; if (lengthDialog < lengthFormat) { throw new InvalidAssSubException("Incorrect dialog line : " + info); } if (lengthDialog > lengthFormat) { // The text field contains commas StringJoiner joiner = new StringJoiner(Events.SEP); for (int i = lengthFormat - 1; i < lengthDialog; i++) { joiner.add(dialogLine[i]); } dialogLine[lengthFormat - 1] = joiner.toString(); dialogLine = Arrays.copyOfRange(dialogLine, 0, lengthFormat); } events.add(parseDialog(eventsFormat, dialogLine)); } line = markAndRead(br); } reset(br, line); return events; } /** * Parse the style section from the reader.
*

* Example of style section: * *

     * [V4+ Styles]
     * Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour
     * Style: StyleOne,Arial,16,64250,16777215,0
     * Style: StyleTwo,Arial,16,16383999,16777215,0
     * 
* * @param br: the buffered reader * @throws IOException * @throws InvalidAssSubException */ private static List parseStyle(BufferedReader br) throws IOException, InvalidAssSubException { String[] styleFormat = findFormat(br, "styles"); List styles = new ArrayList<>(); String line = readFirstTextLine(br); int index = 1; while (line != null && !StrUtil.startWith(line, StrUtil.BRACKET_START)) { if (line.startsWith(V4Style.STYLE) && !line.startsWith(COMMENTS_MARK)) { String[] textLine = line.split(StrUtil.COLON); if (textLine.length > 1) { String[] styleLine = textLine[1].split(V4Style.SEP); styles.add(parseV4Style(styleFormat, styleLine, index)); index++; } } line = markAndRead(br); } while (line != null && !StrUtil.startWith(line, StrUtil.BRACKET_START)) { if (StrUtil.startWith(line, V4Style.STYLE)) { List textLine = StrUtil.split(line, StrUtil.COLON); if (!textLine.isEmpty()) { String[] styleLine = StrUtil.splitToArray(textLine.get(1), V4Style.SEP); styles.add(parseV4Style(styleFormat, styleLine, index)); index++; } } } reset(br, line); return styles; } /** * Return the Events object from text dialog line * * @param eventsFormat: the format definition * @param dialogLine: the dialog line * @return the Events object * @throws InvalidAssSubException */ private static Events parseDialog(String[] eventsFormat, String[] dialogLine) throws InvalidAssSubException { Events events = new Events(); for (int i = 0; i < eventsFormat.length; i++) { String property = StringUtils.uncapitalize(eventsFormat[i].trim()); String value = dialogLine[i].trim(); try { switch (property) { case "start": events.getTime().setStart(ASSTime.fromString(value)); break; case "end": events.getTime().setEnd(ASSTime.fromString(value)); break; case "text": List textLines = Arrays.asList(value.split("\\\\N")); events.setTextLines(new ArrayList<>(textLines)); break; default: String error = callProperty(events, property, value); if (error != null) { throw new InvalidAssSubException(StrUtil.format("Invalid property ({}) {}", property, value)); } break; } } catch (DateTimeException e) { throw new InvalidAssSubException(StrUtil.format("Invalid property ({}) {}", property, value)); } } return events; } /** * Return the V4Style object from text style line * * @param styleFormat: format line * @param styleLine: the style line * @param lineIndex: the line index * @return the style object * @throws InvalidAssSubException */ private static V4Style parseV4Style(String[] styleFormat, String[] styleLine, int lineIndex) throws InvalidAssSubException { String message = "Style at index " + lineIndex + ": "; if (styleFormat.length != styleLine.length) { throw new InvalidAssSubException(message + "does not match style definition"); } V4Style style = new V4Style(); for (int i = 0; i < styleFormat.length; i++) { String property = StringUtils.uncapitalize(styleFormat[i].trim()); String value = styleLine[i].trim(); if (StrUtil.containsIgnoreCase(property, "colour")) { try { Integer.parseInt(value); } catch (NumberFormatException e) { int bgr = getBGR(value); if (bgr != -1) { value = Integer.toString(bgr); } } } String error = callProperty(style, property, value); if (error != null) { throw new InvalidAssSubException(message + error); } } if (StrUtil.isEmpty(style.getName())) { throw new InvalidAssSubException(message + " missing name"); } return style; } /** * Get the BGR code from the &HBBGGRR or &HAABBGGRR pattern * * @param value: the value to convert * @return the bgr code */ private static int getBGR(String value) { int length = value.length(); int bgr = -1; if (length == 10) { // From ASS bgr = ColorUtils.HAABBGGRRToBGR(value); } else if (length == 8) { // From SSA bgr = ColorUtils.HBBGGRRToBGR(value); } return bgr; } /** * Parse the script info section from the reader.
*

* Example of script info section: * *

     * [Script Info]
     * ScriptType: v4.00+
     * Collisions: Normal
     * Timer: 100,0000
     * Title: My movie title
     * 
* * @param br: the buffered reader * @throws IOException * @throws InvalidAssSubException */ private static ScriptInfo parseScriptInfo(BufferedReader br) throws IOException, InvalidAssSubException { ScriptInfo scriptInfo = new ScriptInfo(); String line = readFirstTextLine(br); while (line != null && !StrUtil.startWith(line, StrUtil.BRACKET_START)) { if (!line.startsWith(COMMENTS_MARK)) { String[] split = line.split(ScriptInfo.SEP); if (split.length > 1) { String property = StrUtil.lowerFirst(StrUtil.cleanBlank(split[0])); StringJoiner joiner = new StringJoiner(ScriptInfo.SEP); for (int i = 1; i < split.length; i++) { joiner.add(split[i]); } String value = joiner.toString().trim(); String error = callProperty(scriptInfo, property, value); if (error != null) { throw new InvalidAssSubException("Script info : " + error); } } } line = markAndRead(br); } reset(br, line); return scriptInfo; } /** * Call a specific property of an object with reflection * * @param object: the object to set a property * @param property: the property to define * @param value: the value to set * @return the error message if an error has occured, null otherwise */ private static String callProperty(Object object, String property, String value) { String error = null; PropertyDescriptor descriptor = BeanUtil.getPropertyDescriptor(object.getClass(), property); if (descriptor != null) { String type = descriptor.getPropertyType().getSimpleName(); switch (type) { case "String": BeanUtil.setProperty(object, property, value); break; case "int": BeanUtil.setProperty(object, property, Convert.toInt(value)); break; case "boolean": BeanUtil.setProperty(object, property, Convert.toBool(value)); break; case "double": BeanUtil.setProperty(object, property, Convert.toDouble(StrUtil.replace(value, StrUtil.COMMA, StrUtil.DOT))); break; default: break; } } return error; } /** * Get the format string definition * * @param br: the buffered reader * @param sectionName: the name of the section to parse * @return the format string definition * @throws IOException * @throws InvalidAssSubException */ private static String[] findFormat(BufferedReader br, String sectionName) throws IOException, InvalidAssSubException { String line = readFirstTextLine(br); if (StrUtil.isEmpty(line)) { throw new InvalidAssSubException("Missing format definition in " + sectionName + " section"); } if (!StrUtil.startWith(line.trim(), ASSSub.FORMAT)) { throw new InvalidAssSubException(StrUtil.upperFirst(sectionName) + " definition must start with 'Format' line"); } return StrUtil.splitToArray(findInfo(line, ASSSub.FORMAT), V4Style.SEP); } /** * Find the information after ":" in a text line * * @param line: the line * @param search: the information to search * @return info or null if the info is empty / not found */ private static String findInfo(String line, String search) { if (StrUtil.startWithIgnoreCase(line.trim(), search) && line.indexOf(StrUtil.COLON) > 0) { return line.substring(line.indexOf(StrUtil.COLON) + 1).trim(); } return null; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/BaseParser.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.TypeUtil; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidFileException; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSubException; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import org.fordes.subtitles.view.utils.submerge.utils.EncodeUtils; import java.io.*; import java.lang.reflect.Type; import java.nio.charset.Charset; public abstract class BaseParser implements SubtitleParser { /** * UTF-8 BOM Marker */ private static final char BOM_MARKER = '\ufeff'; @Override public T parse(File file) { try { return parse(file, EncodeUtils.guessEncoding(file)); } catch (Exception e) { throw new RuntimeException(e); } } @Override public T parse(File file, String charset) { if (!file.isFile()) { throw new InvalidFileException("File " + file.getName() + " is invalid"); } try (FileInputStream fis = new FileInputStream(file)) { return parse(fis, file.getName(), charset); } catch (Exception e) { throw new RuntimeException(e); } } @SuppressWarnings("unchecked") @Override public T parse(InputStream is, String fileName) { try { return parse(is, fileName, EncodeUtils.guessEncoding(is)); } catch (IOException e) { throw new RuntimeException(e); } } @Override public T parse(InputStream is, String fileName, String charset) { try { Type type = TypeUtil.getTypeArgument(this.getClass().getGenericSuperclass()); T sub = ReflectUtil.newInstance((Class) type); sub.setFileName(fileName); try (BufferedReader br = IoUtil.getReader(is, Charset.forName(charset))) { skipBom(br); parse(br, sub); } return sub; } catch (IOException e) { throw new InvalidFileException(e); } } @Override public T parse(String str, String fileName) { try { Type type = TypeUtil.getTypeArgument(this.getClass().getGenericSuperclass()); T sub = ReflectUtil.newInstance((Class) type); sub.setFileName(fileName); try (BufferedReader br = IoUtil.getReader(StrUtil.getReader(str))) { skipBom(br); parse(br, sub); } return sub; } catch (IOException e) { throw new InvalidFileException(e); } } /** * Parse the subtitle file into a ParsableSubtitle object * * @param br: the buffered reader * @param sub : the subtitle object to fill * @throws IOException * @throws InvalidSubException if an error has occured when parsing the subtitle file */ protected abstract void parse(BufferedReader br, T sub) throws IOException; /** * Ignore blank spaces and return the first text line * * @param br: the buffered reader * @throws IOException */ protected static String readFirstTextLine(BufferedReader br) throws IOException { String line = null; while ((line = br.readLine()) != null) { if (!StrUtil.isEmpty(line.trim())) { break; } } return line; } /** * Remove the byte order mark if exists * * @param br: the buffered reader * @throws IOException */ private static void skipBom(BufferedReader br) throws IOException { br.mark(4); if (BOM_MARKER != br.read()) { br.reset(); } } /** * Reset the reader at the previous mark if the current line is a new section * * @param br: the reader * @param line: the current line * @throws IOException */ protected static void reset(BufferedReader br, String line) throws IOException { if (StrUtil.startWith(line, StrUtil.C_BRACKET_START)) { br.reset(); } } /** * Mark the position in the reader and read the next text line * * @param br: the buffered reader * @return the next text line * @throws IOException */ protected static String markAndRead(BufferedReader br) throws IOException { br.mark(32); return readFirstTextLine(br); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/LRCParser.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.utils.submerge.subtitle.lrc.LRCLine; import org.fordes.subtitles.view.utils.submerge.subtitle.lrc.LRCSub; import org.fordes.subtitles.view.utils.submerge.subtitle.lrc.LRCTime; import java.io.BufferedReader; import java.io.IOException; import java.util.List; /** * @author fordes on 2022/7/21 */ @Slf4j public final class LRCParser extends BaseParser { @Override protected void parse(BufferedReader br, LRCSub sub) throws IOException { boolean found = true; String lineStr = readFirstTimeLine(br); while (found) { String timeStr = StrUtil.subBetween(lineStr, StrUtil.BRACKET_START, StrUtil.BRACKET_END); LRCTime time = LRCTime.fromString(timeStr); List texts = CollUtil.newArrayList(time == null ? lineStr : lineStr.substring(10)); LRCLine line = new LRCLine(time, texts); try { lineStr = br.readLine(); while (lineStr != null && !StrUtil.startWith(lineStr, StrUtil.C_BRACKET_START)) { texts.add(lineStr); lineStr = br.readLine(); } sub.add(line); found = (lineStr != null); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); found = false; } } } /** * 获得首个有效行,即第一个形如:[00:00:00.000]的行 * * @param br * @return * @throws IOException */ private String readFirstTimeLine(BufferedReader br) throws IOException { String lineStr = br.readLine(); while (lineStr != null && !StrUtil.startWith(lineStr, StrUtil.C_BRACKET_START)) { lineStr = br.readLine(); } return lineStr; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ParserFactory.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser; import cn.hutool.core.util.StrUtil; public final class ParserFactory { /** * Return the subtitle parser for the subtitle format matching the extension * * @param extension the subtitle extention * @return the subtitle parser, null if no matching parser */ public static SubtitleParser getParser(String extension) throws Exception { SubtitleParser parser = null; if (StrUtil.equalsAnyIgnoreCase(extension, "ass", "ssa")) { return new ASSParser(); } else if (StrUtil.equalsIgnoreCase(extension, "srt")) { return new SRTParser(); } else if (StrUtil.equalsIgnoreCase(extension, "lrc")) { return new LRCParser(); } throw new Exception(extension + " format not supported"); } /** * Private constructor */ private ParserFactory() { throw new AssertionError(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SRTParser.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser; import cn.hutool.core.util.StrUtil; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSRTSubException; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSubException; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTLine; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTSub; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime; import java.io.BufferedReader; import java.io.IOException; import java.time.LocalTime; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; /** * Parse SRT subtitles */ public final class SRTParser extends BaseParser { @Override protected void parse(BufferedReader br, SRTSub sub) throws IOException, InvalidSubException { boolean found = true; while (found) { SRTLine line = firstIn(br); if (found = (line != null)) { sub.add(line); } } } /** * Extract the firt SRTLine found in a buffered reader.
* * Example of SRT line: * *
	 * 1
	 * 00:02:46,813 --> 00:02:50,063
	 * A text line
	 * 
* * @param br * @return SRTLine the line extracted, null if no SRTLine found * @throws IOException * @throws InvalidSRTSubException */ private static SRTLine firstIn(BufferedReader br) throws IOException, InvalidSRTSubException { String idLine = readFirstTextLine(br); String timeLine = br.readLine(); if (idLine == null || timeLine == null) { return null; } int id = parseId(idLine); SRTTime time = parseTime(timeLine); List textLines = new ArrayList<>(); String testLine; while ((testLine = br.readLine()) != null) { if (StrUtil.isEmpty(testLine.trim())) { break; } textLines.add(testLine); } return new SRTLine(id, time, textLines); } /** * Extract a subtitle id from string * * @param textLine ex 1 * @return the id extracted * @throws InvalidSRTSubException */ private static int parseId(String textLine) throws InvalidSRTSubException { int idSRTLine; try { idSRTLine = Integer.parseInt(textLine.trim()); } catch (NumberFormatException e) { throw new InvalidSRTSubException("Expected id not found -> " + textLine); } return idSRTLine; } /** * Extract a subtitle time from string * * @param timeLine: ex 00:02:08,822 --> 00:02:11,574 * @return the SRTTime object * @throws InvalidSRTSubException */ public static SRTTime parseTime(String timeLine) throws InvalidSRTSubException { SRTTime time = null; String times[] = timeLine.split(SRTTime.DELIMITER.trim()); if (times.length != 2) { throw new InvalidSRTSubException("Subtitle " + timeLine + " - invalid times : " + timeLine); } try { LocalTime start = SRTTime.fromString(times[0]); LocalTime end = SRTTime.fromString(times[1]); time = new SRTTime(start, end); } catch (DateTimeParseException e) { throw new InvalidSRTSubException("Invalid time string : " + timeLine, e); } return time; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SubtitleParser.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidFileException; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidSubException; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import java.io.File; import java.io.InputStream; public interface SubtitleParser { /** * Parse a subtitle file and return the corresponding subtitle object * * @param file the subtitle file * @return the subtitle object * @throws InvalidSubException if the subtitle is not valid * @throws InvalidFileException if the file is not valid */ TimedTextFile parse(File file); /** * Parse a subtitle file from an inputstream and return the corresponding subtitle * object * * @param is the input stream * @param fileName the fileName * @return the subtitle object * @throws InvalidSubException if the subtitle is not valid * @throws InvalidFileException if the file is not valid */ TimedTextFile parse(InputStream is, String fileName); /** * Parse a subtitle file and return the corresponding subtitle object * * @param file the file * @param charset the file charset * @return the subtitle object * @throws InvalidSubException if the subtitle is not valid * @throws InvalidFileException if the file is not valid */ TimedTextFile parse(File file, String charset); /** * Parse a subtitle file from an string and return the corresponding subtitle * object * * @param is the input stream * @param fileName the fileName * @parse charset the file charset * @return the subtitle object * @throws InvalidSubException if the subtitle is not valid * @throws InvalidFileException if the file is not valid */ TimedTextFile parse(InputStream is, String fileName, String charset); /** * Parse a subtitle file from an string and return the corresponding subtitle object * * @param str the subtitle string * @param fileName the fileName * @return the subtitle object */ TimedTextFile parse(String str, String fileName); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidAssSubException.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser.exception; public class InvalidAssSubException extends InvalidSubException { private static final long serialVersionUID = 8942033846085284666L; public InvalidAssSubException() { } public InvalidAssSubException(String arg0) { super(arg0); } public InvalidAssSubException(Throwable arg0) { super(arg0); } public InvalidAssSubException(String arg0, Throwable arg1) { super(arg0, arg1); } public InvalidAssSubException(String arg0, Throwable arg1, boolean arg2, boolean arg3) { super(arg0, arg1, arg2, arg3); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidColorCode.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser.exception; public class InvalidColorCode extends RuntimeException { private static final long serialVersionUID = -4904697807940273825L; public InvalidColorCode() { } public InvalidColorCode(String message) { super(message); } public InvalidColorCode(Throwable cause) { super(cause); } public InvalidColorCode(String message, Throwable cause) { super(message, cause); } public InvalidColorCode(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidFileException.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser.exception; public class InvalidFileException extends RuntimeException { private static final long serialVersionUID = -943455563476464982L; public InvalidFileException() { } public InvalidFileException(String message) { super(message); } public InvalidFileException(Throwable cause) { super(cause); } public InvalidFileException(String message, Throwable cause) { super(message, cause); } public InvalidFileException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSRTSubException.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser.exception; public class InvalidSRTSubException extends InvalidSubException { private static final long serialVersionUID = -8672533341983848962L; public InvalidSRTSubException() { } public InvalidSRTSubException(String arg0) { super(arg0); } public InvalidSRTSubException(Throwable arg0) { super(arg0); } public InvalidSRTSubException(String arg0, Throwable arg1) { super(arg0, arg1); } public InvalidSRTSubException(String arg0, Throwable arg1, boolean arg2, boolean arg3) { super(arg0, arg1, arg2, arg3); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSubException.java ================================================ package org.fordes.subtitles.view.utils.submerge.parser.exception; public class InvalidSubException extends RuntimeException { private static final long serialVersionUID = -8431409375872882596L; public InvalidSubException() { } public InvalidSubException(String arg0) { super(arg0); } public InvalidSubException(Throwable arg0) { super(arg0); } public InvalidSubException(String arg0, Throwable arg1) { super(arg0, arg1); } public InvalidSubException(String arg0, Throwable arg1, boolean arg2, boolean arg3) { super(arg0, arg1, arg2, arg3); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSSub.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.ass; import lombok.Data; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; /** * The class ASSSub represents a SubStation Alpha subtitle * */ @Data public class ASSSub implements TimedTextFile, Serializable { /** * Serial */ private static final long serialVersionUID = 8812933867812351549L; /** * Format */ public static final String FORMAT = "Format"; /** * Events section */ private static final String EVENTS = "[Events]"; /** * Styles section */ private static final String V4_STYLES = "[V4+ Styles]"; /** * Script info section */ private static final String SCRIPT_INFO = "[Script Info]"; /** * Line separator */ private static final String NEW_LINE = "\n"; /** * Key / Value info separator. Ex : "Color: red" */ public static final String SEP = ": "; /** * Subtitle name */ private String filename; /** * Headers and general information about the script */ private ScriptInfo scriptInfo = new ScriptInfo(); /** * Style definitions required by the script */ private List style = new ArrayList<>(); /** * Events for the script - all the subtitles, comments, pictures, sounds, movies and * commands */ private Set events = new TreeSet<>(); @Override public String toString() { StringBuilder sb = new StringBuilder(); // [Script Info] sb.append(SCRIPT_INFO).append(NEW_LINE).append(this.scriptInfo.toString()); sb.append(NEW_LINE).append(NEW_LINE); // [V4 Styles] sb.append(V4_STYLES).append(NEW_LINE); sb.append(FORMAT).append(SEP).append(V4Style.FORMAT_STRING).append(NEW_LINE); this.style.forEach(s -> sb.append(s.toString()).append(NEW_LINE)); sb.append(NEW_LINE); // [Events] sb.append(EVENTS).append(NEW_LINE); sb.append(FORMAT).append(SEP).append(Events.FORMAT_STRING).append(NEW_LINE); this.events.forEach(e -> sb.append(e.toString()).append(NEW_LINE)); return sb.toString(); } /** * Get the ass file as an input stream * * @return the file */ public InputStream toInputStream() { return new ByteArrayInputStream(toString().getBytes()); } @Override public void setFileName(String fileName) { this.filename = fileName; } @Override public String getFileName() { return this.filename; } @Override public Set getTimedLines() { return this.events; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSTime.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.ass; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; /** * The class ASSTime represents a SubStation Alpha time : meaning the time at * which the text will appear and disappear onscreen * */ public class ASSTime extends SubtitleTime { /** * Serial */ private static final long serialVersionUID = -8393452818120120069L; /** * The time pattern */ public static final String TIME_PATTERN = "H:mm:ss.SS"; /** * The time pattern formatter */ public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN); /** * Constructor */ public ASSTime(LocalTime start, LocalTime end) { super(start, end); } /** * Constructor */ public ASSTime() { super(); } /** * Convert a LocalTime to string * * @param time: the time to format * @return the formatted time */ public static String format(LocalTime time) { return time.format(FORMATTER); } /** * Convert a string pattern to a Local time * * @param time * @return * @throws DateTimeParseException */ public static LocalTime fromString(String time) { return LocalTime.parse(time.replace(',', '.'), FORMATTER); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/Events.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.ass; import cn.hutool.core.util.StrUtil; import lombok.Data; import lombok.EqualsAndHashCode; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine; import java.io.Serializable; import java.util.List; /** * Contain the subtitle text, their timings, and how it should be displayed. The fields * which appear in each Dialogue line are defined by a Format: line, which must appear * before any events in the section. The format line specifies how SSA will interpret all * following Event lines. * * The field names must be spelled correctly, and are as follows: * * Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text * * The last field will always be the Text field, so that it can contain commas. The format * line allows new fields to be added to the script format in future, and yet allow old * versions of the software to read the fields it recognises - even if the field order is * changed. */ @Data @EqualsAndHashCode(callSuper = true) public class Events extends SubtitleLine implements Serializable{ /** * Serial */ private static final long serialVersionUID = -6706119890451628726L; /** * Format declaration */ public static final String FORMAT_STRING = "Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"; /** * New line separator */ private static final String ESCAPED_RETURN = "\\N"; /** * Dialog */ public static final String DIALOGUE = "Dialogue: "; /** * Separator */ public static final String SEP = ","; /** * Subtitles having different layer number will be ignored during the collusion * detection. * * Higher numbered layers will be drawn over the lower numbered. */ private int layer; /** * Style name. If it is "Default", then your own *Default style will be subtituted. * * However, the Default style used by the script author IS stored in the script even * though SSA ignores it - so if you want to use it, the information is there - you * could even change the Name in the Style definition line, so that it will appear in * the list of "script" styles. */ private String style; /** * Character name. This is the name of the character who speaks the dialogue. It is * for information only, to make the script is easier to follow when editing/timing. */ private String name = StrUtil.EMPTY; /** * 4-figure Left Margin override. The values are in pixels. All zeroes means the * default margins defined by the style are used. */ private String marginL = "0000"; /** * 4-figure Right Margin override. The values are in pixels. All zeroes means the * default margins defined by the style are used. */ private String marginR = "0000"; /** * 4-figure Bottom Margin override. The values are in pixels. All zeroes means the * default margins defined by the style are used. */ private String marginV = "0000"; /** * Transition Effect. This is either empty, or contains information for one of the * three transition effects implemented in SSA v4.x * * The effect names are case sensitive and must appear exactly as shown. The effect * names do not have quote marks around them. * * "Scroll up;y1;y2;delay[;fadeawayheight]"means that the text/picture will scroll up * the screen. The parameters after the words "Scroll up" are separated by semicolons. * * “Banner;delay” means that text will be forced into a single line, regardless of * length, and scrolled from right to left accross the screen. */ private String effect = StrUtil.EMPTY; /** * Constructor * * @param style style name to apply * @param time Start Time of the Event * @param textLines End Time of the Event */ public Events(String style, ASSTime time, List textLines) { this.style = style; this.time = time; this.textLines = textLines; } /** * Constructor * */ public Events() { super(); this.style = StrUtil.EMPTY; this.time = new ASSTime(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(DIALOGUE); sb.append(this.layer).append(SEP); sb.append(ASSTime.format(this.time.getStart())).append(SEP); sb.append(ASSTime.format(this.time.getEnd())).append(SEP); sb.append(this.style).append(SEP); sb.append(this.name).append(SEP); sb.append(this.marginL).append(SEP); sb.append(this.marginR).append(SEP); sb.append(this.marginV).append(SEP); sb.append(this.effect).append(SEP); this.textLines.forEach(tl -> sb.append(tl.toString()).append(ESCAPED_RETURN)); return StrUtil.removeSuffix(sb.toString(), ESCAPED_RETURN); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ScriptInfo.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.ass; import lombok.Data; import java.io.Serializable; import java.text.DecimalFormat; /** * The ScriptInfo section contains headers and general information about the * script */ @Data public class ScriptInfo implements Serializable { /** * Serial */ private static final long serialVersionUID = -6613873382621648995L; /** * Timer declaration */ private static final String TIMER = "Timer"; /** * PlayDepth declaration */ private static final String PLAY_DEPTH = "PlayDepth"; /** * PlayResX declaration */ private static final String PLAY_RES_X = "PlayResX"; /** * PlayResY declaration */ private static final String PLAY_RES_Y = "PlayResY"; /** * Collisions declaration */ private static final String COLLISIONS = "Collisions"; /** * Script Type declaration */ private static final String SCRIPT_TYPE = "ScriptType"; /** * Update Details declaration */ private static final String UPDATE_DETAILS = "Update Details"; /** * Script Updated By declaration */ private static final String SCRIPT_UPDATED_BY = "Script Updated By"; /** * Synch Point declaration */ private static final String SYNCH_POINT = "Synch Point"; /** * Original Timing declaration */ private static final String ORIGINAL_TIMING = "Original Timing"; /** * Original Editing declaration */ private static final String ORIGINAL_EDITING = "Original Editing"; /** * Original Translation declaration */ private static final String ORIGINAL_TRANSLATION = "Original Translation"; /** * Original Script declaration */ private static final String ORIGINAL_SCRIPT = "Original Script"; /** * Title declaration */ private static final String TITLE = "Title"; /** * Separator */ public static final String SEP = ": "; /** * New line separator */ private static final String NEW_LINE = "\n"; /** * Decimal time formater */ private static final DecimalFormat timeFormatter = new DecimalFormat("#.0000"); public enum Collision { /** * position subtitles in the position specified by the "margins" */ NORMAL("Normal"), /** * subtitles will be shifted upwards to make room for subsequent overlapping * subtitles */ REVERSE("Reverse"); private String type; Collision(String type) { this.type = type; } @Override public String toString() { return this.type; } } /** * This is a description of the script. If the original author(s) did not provide this * information then is automatically substituted. */ private String title; /** * The original author(s) of the script. If the original author(s) did not provide * this information then is automatically substituted. */ private String originalScript; /** * (optional) The original translator of the dialogue. This entry does not appear if * no information was entered by the author. */ private String originalTranslation; /** * (optional) The original script editor(s), typically whoever took the raw * translation and turned it into idiomatic english and reworded for readability. This * entry does not appear if no information was entered by the author. */ private String originalEditing; /** * (optional) Whoever timed the original script. This entry does not appear if no * information was entered by the author. */ private String originalTiming; /** * (optional) Description of where in the video the script should begin playback. */ private String synchPoint; /** * (optional) The original script editor(s), typically whoever took the raw * translation and turned it into idiomatic english and reworded for readability. This * entry does not appear if no information was entered by the author. */ private String originalScriptChecking; /** * (optional) Names of any other subtitling groups who edited the original script. */ private String scriptUpdatedBy; /** * The details of any updates to the original script made by other subtilting groups. */ private String userDetails; /** * This is the SSA script format version eg. "V4.00". It is used by SSA to give a * warning if you are using a version of SSA older than the version that created the * script. */ private String scriptType = "v4.00+"; /** * This determines how subtitles are moved, when automatically preventing onscreen * collisions. * * If the entry says "Normal" then SSA will attempt to position subtitles in the * position specified by the "margins". However, subtitles can be shifted vertically * to prevent onscreen collisions. With "normal" collision prevention, the subtitles * will "stack up" one above the other - but they will always be positioned as close * the vertical (bottom) margin as possible - filling in "gaps" in other subtitles if * one large enough is available. * * If the entry says "Reverse" then subtitles will be shifted upwards to make room for * subsequent overlapping subtitles. This means the subtitles can nearly always be * read top-down - but it also means that the first subtitle can appear half way up * the screen before the subsequent overlapping subtitles appear. It can use a lot of * screen area. */ private Collision collisions = Collision.NORMAL; /** * This is the height of the screen used by the script's author(s) when playing the * script. SSA v4 will automatically select the nearest enabled setting, if you are * using Directdraw playback. */ private int playResY; /** * This is the width of the screen used by the script's author(s) when playing the * script. SSA will automatically select the nearest enabled, setting if you are using * Directdraw playback. */ private int playResX; /** * This is the colour depth used by the script's author(s) when playing the script. * SSA will automatically select the nearest enabled setting if you are using * Directdraw playback. */ private int playDepth; /** * This is the Timer Speed for the script, as a percentage. */ private double timer = 100.0000; @Override public String toString() { StringBuilder sb = new StringBuilder(); appendNotNull(sb, TITLE, this.title); appendNotNull(sb, ORIGINAL_SCRIPT, this.originalScript); appendNotNull(sb, ORIGINAL_TRANSLATION, this.originalTranslation); appendNotNull(sb, ORIGINAL_EDITING, this.originalEditing); appendNotNull(sb, ORIGINAL_TIMING, this.originalTiming); appendNotNull(sb, SYNCH_POINT, this.synchPoint); appendNotNull(sb, SCRIPT_UPDATED_BY, this.scriptUpdatedBy); appendNotNull(sb, UPDATE_DETAILS, this.userDetails); appendNotNull(sb, SCRIPT_TYPE, this.scriptType); appendNotNull(sb, COLLISIONS, this.collisions.toString()); appendPositive(sb, PLAY_RES_Y, this.playResY); appendPositive(sb, PLAY_RES_X, this.playResX); appendPositive(sb, PLAY_DEPTH, this.playDepth); sb.append(TIMER).append(SEP).append(timeFormatter.format(this.timer)); return sb.toString(); } // ======================= private methods ======================= /** * Append a value in a StringBuilder if the value is not null * * @param sb: the string builder * @param desc: the description * @param val: the value */ private static void appendNotNull(StringBuilder sb, String desc, String val) { if (val != null) { sb.append(desc).append(SEP).append(val).append(NEW_LINE); } } /** * Append a value in a StringBuilder if the value is positive * * @param sb: the string builder * @param desc: the description * @param val: the value */ private static void appendPositive(StringBuilder sb, String desc, int val) { if (val > 0) { sb.append(desc).append(SEP).append(val).append(NEW_LINE); } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/V4Style.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.ass; import lombok.Data; import java.io.Serializable; /** * Styles define the appearance and position of subtitles. All styles used by the script * are are defined by a Style line in the script. * * Any of the the settings in the Style, (except shadow/outline type and depth) can * overridden by control codes in the subtitle text. * * The fields which appear in each Style definition line are named in a special line with * the line type “Format:”. The Format line must appear before any Styles - because it * defines how SSA will interpret the Style definition lines. The field names listed in * the format line must be correctly spelled! * * The fields are as follows: * * Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, * Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, * Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding * * The format line allows new fields to be added to the script format in future, and yet * allow old versions of the software to read the fields it recognises - even if the field * order is changed. */ @Data public class V4Style implements Serializable { /** * Serial */ private static final long serialVersionUID = -4910432063071707768L; /** * Style declaration */ public static final String STYLE = "Style: "; /** * Format declaration */ public static final String FORMAT_STRING = "Name,Fontname,Fontsize,PrimaryColour," + "SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline," + "StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow," + "Alignment,MarginL,MarginR,MarginV,Encoding"; /** * Separator */ public static final String SEP = ","; /** * The name of the Style. Case sensitive. Cannot include commas. */ private String name; /** * The fontname as used by Windows. Case-sensitive. */ private String fontname = "Arial"; /** * The font size */ private int fontsize; /** * A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal * equivelent of this number is BBGGRR * * The color format contains the alpha channel, too. (AABBGGRR) */ private int primaryColour; /** * long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal * equivelent of this number is BBGGRR * * This colour may be used instead of the Primary colour when a subtitle is * automatically shifted to prevent an onscreen collsion, to distinguish the different * subtitles. * * The color format contains the alpha channel, too. (AABBGGRR) */ private int secondaryColour = 16777215; // #FFFFFF (white) /** * A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal * equivelent of this number is BBGGRR * * This colour may be used instead of the Primary or Secondary colour when a subtitle * is automatically shifted to prevent an onscreen collsion, to distinguish the * different subtitles. * * The color format contains the alpha channel, too. (AABBGGRR) */ private int outlineColour; /** * This is the colour of the subtitle outline or shadow, if these are used. A long * integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal * equivelent of this number is BBGGRR. * * The color format contains the alpha channel, too. (AABBGGRR) */ private int backColour; /** * This defines whether text is bold (true) or not (false). -1 is True, 0 is False. * This is independant of the Italic attribute - you can have have text which is both * bold and italic. */ private boolean bold; /** * This defines whether text is italic (true) or not (false). -1 is True, 0 is False. * This is independant of the bold attribute - you can have have text which is both * bold and italic. */ private boolean italic; /** * -1 is True, 0 is False */ private boolean underline; /** * -1 is True, 0 is False */ private boolean strikeOut; /** * Modifies the width of the font. [percent] */ private int scaleX = 100; /** * Modifies the height of the font. [percent] */ private int scaleY = 100; /** * Extra space between characters. [pixels] */ private int spacing; /** * The origin of the rotation is defined by the alignment. Can be a floating point * number. [degrees] */ private double angle; /** * 1=Outline + drop shadow, 3=Opaque box */ private int borderStyle = 1; /** * If BorderStyle is 1, then this specifies the width of the outline around the text, * in pixels. Values may be 0, 1, 2, 3 or 4. */ private int outline = 2; /** * If BorderStyle is 1, then this specifies the depth of the drop shadow behind the * text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in * addition to an outline - SSA will force an outline of 1 pixel if no outline width * is given. */ private int shadow; /** * This sets how text is "justified" within the Left/Right onscreen margins, and also * the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value * for a "Toptitle". Add 8 to the value for a "Midtitle". eg. 5 = left-justified * toptitle */ private int alignment = 2; /** * This defines the Left Margin in pixels. It is the distance from the left-hand edge * of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas * in which the subtitle text will be displayed. */ private int marginL = 10; /** * This defines the Right Margin in pixels. It is the distance from the right-hand * edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) define * areas in which the subtitle text will be displayed. */ private int marginR = 10; /** * This defines the vertical Left Margin in pixels. For a subtitle, it is the distance * from the bottom of the screen. For a toptitle, it is the distance from the top of * the screen. For a midtitle, the value is ignored - the text will be vertically * centred */ private int marginV = 10; /** * This specifies the font character set or encoding and on multi-lingual Windows * installations it provides access to characters used in multiple than one languages. * It is usually 0 (zero) for English (Western, ANSI) Windows. * * When the file is Unicode, this field is useful during file format conversions. */ private int encoding; /** * Default constructor */ public V4Style() { } /** * Constructor * * @param name: the style name */ public V4Style(String name) { this.name = name; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(STYLE); sb.append(this.name).append(SEP); sb.append(this.fontname).append(SEP); sb.append(this.fontsize).append(SEP); sb.append(this.primaryColour).append(SEP); sb.append(this.secondaryColour).append(SEP); sb.append(this.outlineColour).append(SEP); sb.append(this.backColour).append(SEP); sb.append(this.bold ? -1 : 0).append(SEP); sb.append(this.italic ? -1 : 0).append(SEP); sb.append(this.underline ? -1 : 0).append(SEP); sb.append(this.strikeOut ? -1 : 0).append(SEP); sb.append(this.scaleX).append(SEP); sb.append(this.scaleY).append(SEP); sb.append(this.spacing).append(SEP); sb.append(this.angle).append(SEP); sb.append(this.borderStyle).append(SEP); sb.append(this.outline).append(SEP); sb.append(this.shadow).append(SEP); sb.append(this.alignment).append(SEP); sb.append(this.marginL).append(SEP); sb.append(this.marginR).append(SEP); sb.append(this.marginV).append(SEP); sb.append(this.encoding); return sb.toString(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleLine.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.common; import java.io.Serializable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class SubtitleLine implements TimedLine, Serializable { /** * Serial Id */ private static final long serialVersionUID = 288560648398584309L; /** * Subtitle Text. This is the actual text which will be displayed as a subtitle * onscreen. */ protected List textLines = new ArrayList<>(); /** * Timecodes */ protected T time; /** * Comparator that only compare timings * * @return the comparator */ public static Comparator timeComparator = Comparator.comparing(TimedLine::getTime); /** * Constructor */ public SubtitleLine() { super(); } /** * Constructor */ public SubtitleLine(T time) { super(); this.time = time; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } TimedLine other = (TimedLine) obj; return compareTo(other) == 0; } @Override public int compare(TimedLine o1, TimedLine o2) { return o1.compareTo(o2); } @Override public int compareTo(TimedLine o) { if (o.getTime() == null) { return 1; } int compare = this.time.compareTo(o.getTime()); if (compare == 0) { String thisText = String.join(",", this.textLines); String otherText = String.join(",", o.getTextLines()); compare = thisText.compareTo(otherText); } return compare; } // ===================== getter and setter start ===================== @Override public T getTime() { return this.time; } public void setTime(T time) { this.time = time; } @Override public List getTextLines() { return this.textLines; } @Override public void setTextLines(List textLines) { this.textLines = textLines; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleTime.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.common; import java.io.Serializable; import java.time.LocalTime; public class SubtitleTime implements TimedObject, Serializable { private static final long serialVersionUID = -2283115927128309201L; /** * Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is * the time elapsed during script playback at which the text will appear onscreen. */ protected LocalTime start; /** * End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is * the time elapsed during script playback at which the text will disappear offscreen. */ protected LocalTime end; public SubtitleTime() { } public SubtitleTime(LocalTime start, LocalTime end) { super(); this.start = start; this.end = end; } @Override public int compare(TimedObject o1, TimedObject o2) { return o1.compareTo(o2); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } TimedObject other = (TimedObject) obj; return compareTo(other) == 0; } @Override public int compareTo(TimedObject other) { int compare = this.start.compareTo(other.getStart()); if (compare == 0) { compare = this.end.compareTo(other.getEnd()); } return compare; } // ===================== getter and setter start ===================== @Override public LocalTime getStart() { return this.start; } @Override public void setStart(LocalTime start) { this.start = start; } @Override public LocalTime getEnd() { return this.end; } @Override public void setEnd(LocalTime end) { this.end = end; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedLine.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.common; import java.io.Serializable; import java.util.Comparator; import java.util.List; /** * Simple object that contains a text line with a time */ public interface TimedLine extends Serializable, Comparable, Comparator { /** * Get the text lines * * @return textLines */ List getTextLines(); /** * Set the text lines * */ void setTextLines(List textLines); /** * Get the timed object * * @return the time */ TimedObject getTime(); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedObject.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.common; import java.io.Serializable; import java.time.LocalTime; import java.util.Comparator; /** * Simple object that contains timed start ant end */ public interface TimedObject extends Serializable, Comparable, Comparator { /** * Return the time elapsed during script playback at which the text will appear * onscreen. * * @return start time */ LocalTime getStart(); /** * Return the time elapsed during script playback at which the text will disappear * offscreen. * * @return end time */ LocalTime getEnd(); /** * Set the time elapsed during script playback at which the text will appear onscreen. * * @param start time */ void setStart(LocalTime start); /** * Set the time elapsed during script playback at which the text will disappear * offscreen. * * @param end time */ void setEnd(LocalTime end); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedTextFile.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.common; import java.io.Serializable; import java.util.Set; /** * Object that represents a text file containing timed lines */ public interface TimedTextFile extends Serializable { /** * Get the filename * * @return the filename */ String getFileName(); /** * Set the filename * * @param fileName: the filename */ void setFileName(String fileName); /** * Get the timed lines * * @return lines */ Set getTimedLines(); } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/Font.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.config; import lombok.Data; import org.fordes.subtitles.view.utils.submerge.constant.FontName; import java.io.Serializable; @Data public class Font implements Serializable { /** * Serial */ private static final long serialVersionUID = -3711480706383195193L; /** * Font name */ private String name = FontName.Arial.toString(); /** * Font size */ private int size = 16; /** * Font color */ private String color = "#fffff9"; /** * Outline color */ private String outlineColor = "#000000"; /** * Outline width */ private int outlineWidth = 2; } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/SimpleSubConfig.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.config; import lombok.Data; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import java.io.Serializable; @Data public class SimpleSubConfig implements Serializable { private static final long serialVersionUID = -485125721913729063L; private String styleName; private TimedTextFile sub; private Font fontconfig = new Font(); private int alignment; private int verticalMargin = 10; public SimpleSubConfig() { } public SimpleSubConfig(TimedTextFile sub, Font fontConfig) { this.sub = sub; this.fontconfig = fontConfig; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCLine.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.lrc; import cn.hutool.core.util.StrUtil; import lombok.NoArgsConstructor; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine; import java.util.List; /** * @author fordes on 2022/7/21 */ @NoArgsConstructor public class LRCLine extends SubtitleLine { private static final long serialVersionUID = -5787808773967579723L; public LRCLine(LRCTime time, List textLines) { this.time = time; this.textLines = textLines; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(this.time == null ? StrUtil.EMPTY: this.time); textLines.forEach(line -> sb.append(line).append(StrUtil.CR)); return sb.toString(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCSub.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.lrc; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import lombok.Data; import lombok.NoArgsConstructor; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import java.io.Serializable; import java.util.Set; import java.util.TreeSet; /** * @author fordes on 2022/7/21 */ @Data @NoArgsConstructor public class LRCSub implements TimedTextFile, Serializable { private static final long serialVersionUID = -2909833789376537734L; private String fileName; private Set lines = new TreeSet<>(); public void add(LRCLine line) { this.lines.add(line); } public void remove(TimedLine line) { this.lines.remove((LRCLine) line); } public String toString() { return CollUtil.join(lines, StrUtil.EMPTY); } @Override public Set getTimedLines() { return this.lines; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCTime.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.lrc; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.StrUtil; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime; import org.fordes.subtitles.view.utils.submerge.subtitle.srt.SRTTime; import java.io.Serializable; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoField; /** * @author fordes on 2022/7/21 */ @Slf4j @NoArgsConstructor public class LRCTime extends SubtitleTime implements Serializable { private static final long serialVersionUID = -5787808223967579723L; public static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(SRTTime.PATTERN); public static final String PATTERN = "mm:ss.SS"; private static final String TS_PATTERN = "%02d:%02d.%02d"; public LRCTime(LocalTime start) { this.start = start; } @Override public String toString() { return StrUtil.format("[{}]", format(start)); } public static String format(LocalTime time) { int min = time.get(ChronoField.MINUTE_OF_HOUR); int sec = time.get(ChronoField.SECOND_OF_MINUTE); int ms = time.get(ChronoField.MILLI_OF_SECOND); return String.format(TS_PATTERN, min, sec, ms); } public static LRCTime fromString(String times) { try { LocalTime time = LocalDateTimeUtil.parse(times, PATTERN).toLocalTime(); return new LRCTime(time); }catch (Exception e) { return null; } } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTLine.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.srt; import lombok.Data; import lombok.EqualsAndHashCode; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleLine; import java.util.List; /** * Class represents an abstract line of SRT, meaning text, timecodes and index * */ @Data @EqualsAndHashCode(callSuper = false) public class SRTLine extends SubtitleLine { private static final long serialVersionUID = -1220593401999895814L; private static final String NEW_LINE = "\n"; private int id; public SRTLine(int id, SRTTime time, List textLines) { this.id = id; this.time = time; this.textLines = textLines; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(this.id).append(NEW_LINE); sb.append(this.time).append(NEW_LINE); this.textLines.forEach(textLine -> sb.append(textLine).append(NEW_LINE)); return sb.append(NEW_LINE).toString(); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTSub.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.srt; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedTextFile; import java.io.Serializable; import java.util.Set; import java.util.TreeSet; /** * Class represents an SRT file, meandin a complete set of subtitle lines * */ public class SRTSub implements TimedTextFile, Serializable { private static final long serialVersionUID = -2909833999376537734L; private String fileName; private Set lines = new TreeSet<>(); // ======================== Public methods ========================== public void add(SRTLine line) { this.lines.add(line); } public void remove(TimedLine line) { this.lines.remove(line); } @Override public String toString() { StringBuilder sb = new StringBuilder(); this.lines.forEach(srtLine -> sb.append(srtLine)); return sb.toString(); } // ===================== getter and setter start ===================== public Set getLines() { return this.lines; } @Override public Set getTimedLines() { return this.lines; } public void setLines(Set lines) { this.lines = lines; } @Override public String getFileName() { return this.fileName; } @Override public void setFileName(String fileName) { this.fileName = fileName; } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTTime.java ================================================ package org.fordes.subtitles.view.utils.submerge.subtitle.srt; import org.fordes.subtitles.view.utils.submerge.subtitle.common.SubtitleTime; import java.io.Serializable; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoField; public class SRTTime extends SubtitleTime implements Serializable { private static final long serialVersionUID = -5784108223967579723L; public static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(SRTTime.PATTERN); public static final String PATTERN = "HH:mm:ss,SSS"; private static final String TS_PATTERN = "%02d:%02d:%02d,%03d"; public static final String DELIMITER = " --> "; public SRTTime() { super(); } public SRTTime(LocalTime start, LocalTime end) { super(start, end); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(format(this.start)); sb.append(DELIMITER); sb.append(format(this.end)); return sb.toString(); } /** * Convert a LocalTime to string * * @param time: the time to format * @return the formatted time */ public static String format(LocalTime time) { int hr = time.get(ChronoField.HOUR_OF_DAY); int min = time.get(ChronoField.MINUTE_OF_HOUR); int sec = time.get(ChronoField.SECOND_OF_MINUTE); int ms = time.get(ChronoField.MILLI_OF_SECOND); return String.format(TS_PATTERN, hr, min, sec, ms); } /** * Convert a string pattern to a Local time * * @param times * @see SRTTime.PATTERN * @return * @throws DateTimeParseException */ public static LocalTime fromString(String times) { return LocalTime.parse(times.replace('.', ',').trim(), FORMATTER); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ColorUtils.java ================================================ package org.fordes.subtitles.view.utils.submerge.utils; import cn.hutool.core.util.StrUtil; import org.fordes.subtitles.view.utils.submerge.parser.exception.InvalidColorCode; import java.awt.*; public final class ColorUtils { /** * Convert the hexadecimal color code to BGR code * * @param hex * @return rgb */ public static int hexToBGR(String hex) { Color color = Color.decode(hex); int in = Integer.decode(Integer.toString(color.getRGB())); int red = (in >> 16) & 0xFF; int green = (in >> 8) & 0xFF; int blue = (in) & 0xFF; return (blue << 16) | (green << 8) | (red); } /** * Convert a &HAABBGGRR to hexadecimal * * @param haabbggrr: the color code * @return the hexadecimal code * @throws InvalidColorCode */ public static String HAABBGGRRToHex(String haabbggrr) { if (haabbggrr.length() != 10) { throw new InvalidColorCode("Invalid pattern, must be &HAABBGGRR"); } StringBuilder sb = new StringBuilder(); sb.append("#"); sb.append(haabbggrr.substring(8)); sb.append(haabbggrr.charAt(6)); sb.append(haabbggrr.charAt(4)); sb.append(haabbggrr.charAt(2)); return sb.toString().toLowerCase(); } /** * Convert a &HBBGGRR to hexadecimal * * @param hbbggrr: the color code * @return the hexadecimal code */ public static String HBBGGRRToHex(String hbbggrr) { if (hbbggrr.length() != 8) { throw new InvalidColorCode("Invalid pattern, must be &HBBGGRR"); } return StrUtil.concat(false, "#", hbbggrr.substring(6), hbbggrr.substring(4, 5), hbbggrr.substring(2, 3)).toLowerCase(); } /** * Convert a &HAABBGGRR to BGR * * @param haabbggrr: the color code * @return the BGR code * @throws InvalidColorCode */ public static int HAABBGGRRToBGR(String haabbggrr) { return hexToBGR(HAABBGGRRToHex(haabbggrr)); } /** * Convert a &HBBGGRR to BGR * * @param hbbggrr: the color code * @return the BGR code * @throws InvalidColorCode */ public static int HBBGGRRToBGR(String hbbggrr) { return hexToBGR(HBBGGRRToHex(hbbggrr)); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ConvertUtils.java ================================================ package org.fordes.subtitles.view.utils.submerge.utils; import cn.hutool.core.util.StrUtil; import org.fordes.subtitles.view.utils.submerge.subtitle.ass.ASSTime; import org.fordes.subtitles.view.utils.submerge.subtitle.ass.Events; import org.fordes.subtitles.view.utils.submerge.subtitle.ass.V4Style; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedLine; import org.fordes.subtitles.view.utils.submerge.subtitle.common.TimedObject; import org.fordes.subtitles.view.utils.submerge.subtitle.config.Font; import org.fordes.subtitles.view.utils.submerge.subtitle.config.SimpleSubConfig; import java.util.List; import java.util.stream.Collectors; public class ConvertUtils { private static final String RGX_XML_TAG = "<[^>]+>"; private static final String RGX_ASS_FORMATTING = "\\{[^\\}]*\\}"; private static final String SRT_ITALIC_CLOSE = "\\"; private static final String SRT_ITALIC_OPEN = "\\"; private static final String ASS_ITALIC_CLOSE = "\\{\\\\i0\\}"; private static final String ASS_ITALIC_OPEN = "\\{\\\\i1\\}"; /** * Create an Events object from a timed line * * @param line: a timed line * @param style: the style name * @return the corresponding Events */ public static Events createEvent(TimedLine line, String style) { List newLine = line.getTextLines().stream() .map(ConvertUtils::toASSString).collect(Collectors.toList()); TimedObject timeLine = line.getTime(); ASSTime time = new ASSTime(timeLine.getStart(), timeLine.getEnd()); return new Events(style, time, newLine); } /** * Create a V4Style object from SubInput * * @param config: the configuration object * @return the corresponding style */ public static V4Style createV4Style(SimpleSubConfig config) { V4Style style = new V4Style(config.getStyleName()); Font font = config.getFontconfig(); style.setFontname(font.getName()); style.setFontsize(font.getSize()); style.setAlignment(config.getAlignment()); style.setPrimaryColour(ColorUtils.hexToBGR(font.getColor())); style.setOutlineColour(ColorUtils.hexToBGR(font.getOutlineColor())); style.setOutline(font.getOutlineWidth()); style.setMarginV(config.getVerticalMargin()); return style; } /** * Format a text line to be srt compliant * * @param textLine the text line * @return the formatted text line */ public static String toSRTString(String textLine) { String formatted = textLine.replaceAll(ASS_ITALIC_OPEN, SRT_ITALIC_OPEN); formatted = formatted.replaceAll(ASS_ITALIC_CLOSE, SRT_ITALIC_CLOSE); formatted = formatted.replaceAll(RGX_ASS_FORMATTING, StrUtil.EMPTY); return formatted; } /** * Format a text line to be ass compliant * * @param textLine the text line * @return */ public static String toASSString(String textLine) { String formatted = textLine.replaceAll(SRT_ITALIC_OPEN, ASS_ITALIC_OPEN); formatted = formatted.replaceAll(SRT_ITALIC_CLOSE, ASS_ITALIC_CLOSE); return formatted.replaceAll(RGX_XML_TAG, StrUtil.EMPTY); } } ================================================ FILE: src/main/java/org/fordes/subtitles/view/utils/submerge/utils/EncodeUtils.java ================================================ package org.fordes.subtitles.view.utils.submerge.utils; import cn.hutool.core.io.CharsetDetector; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.CharsetUtil; import lombok.extern.slf4j.Slf4j; import org.mozilla.universalchardet.UniversalDetector; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; @Slf4j public class EncodeUtils { /** * Detect charset encoding of a file * * @param file: the file to detect encoding from * @return the charset encoding * @throws IOException */ public static String guessEncoding(File file) throws IOException { try (FileInputStream is = new FileInputStream(file)) { return guessEncoding(is); } } /** * Detect charset encoding of an input stream * * @param is: the InputStream to detect encoding from * @return the charset encoding * @throws IOException */ public static String guessEncoding(InputStream is) throws IOException { //先使用juniversalchardet检测 String code = guessEncoding(IoUtil.readBytes(is)); if (code != null) { return code; } //使用hutool的charset检测 Charset charset = CharsetDetector.detect(is); if (charset != null) { return charset.name(); } //默认使用UTF-8 log.debug("文件编码检测失败,使用默认编码UTF-8"); return CharsetUtil.UTF_8; } /** * Detect charset encoding of a byte array * * @param bytes: the byte array to detect encoding from * @return the charset encoding */ public static String guessEncoding(byte[] bytes) { UniversalDetector detector = new UniversalDetector(null); detector.handleData(bytes, 0, bytes.length); detector.dataEnd(); String encoding = detector.getDetectedCharset(); detector.reset(); return encoding; } } ================================================ FILE: src/main/resources/application.yml ================================================ logging: config: classpath:logback/logback-spring.xml file: path: ./logs spring: application: name: subtitles-view profiles: active: dev datasource: url: jdbc:sqlite::resource:db/subtitles-view.sqlite driver-class-name: org.sqlite.JDBC service: translate: tencent: region: ap-shanghai #接口服务,详见:https://cloud.tencent.com/document/api/551/15615#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 huoshan: region: cn-north-1 #https://www.volcengine.com/docs/6369/67269 version-date: 2020-06-01 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: org.fordes.subtitles.view.mode.PO configuration: map-underscore-to-camel-case: false cache-enabled: true global-config: banner: off db-config: update-strategy: not_null config: editMode: false exitMode: false languageListMode: true fontSize: 18 currentTheme: false ================================================ FILE: src/main/resources/banner.txt ================================================ ${AnsiColor.BRIGHT_GREEN} $$$$$$\ $$\ $$\ $$\ $$\ $$\ $$\ $$\ $$\ ${AnsiColor.BRIGHT_GREEN} $$ __$$\ $$ | $$ | \__| $$ | $$ | $$ | $$ |\__| ${AnsiColor.BRIGHT_GREEN} $$ / \__|$$\ $$\ $$$$$$$\ $$$$$$\ $$\ $$$$$$\ $$ | $$$$$$\ $$$$$$$\ $$ | $$ |$$\ $$$$$$\ $$\ $$\ $$\ ${AnsiColor.BRIGHT_GREEN} \$$$$$$\ $$ | $$ |$$ __$$\\_$$ _| $$ |\_$$ _| $$ |$$ __$$\ $$ _____| \$$\ $$ |$$ |$$ __$$\ $$ | $$ | $$ | ${AnsiColor.BRIGHT_GREEN} \____$$\ $$ | $$ |$$ | $$ | $$ | $$ | $$ | $$ |$$$$$$$$ |\$$$$$$\ \$$\$$ / $$ |$$$$$$$$ |$$ | $$ | $$ | ${AnsiColor.BRIGHT_GREEN} $$\ $$ |$$ | $$ |$$ | $$ | $$ |$$\ $$ | $$ |$$\ $$ |$$ ____| \____$$\ \$$$ / $$ |$$ ____|$$ | $$ | $$ | ${AnsiColor.BRIGHT_GREEN} \$$$$$$ |\$$$$$$ |$$$$$$$ | \$$$$ |$$ | \$$$$ |$$ |\$$$$$$$\ $$$$$$$ | \$ / $$ |\$$$$$$$\ \$$$$$\$$$$ | ${AnsiColor.BRIGHT_GREEN} \______/ \______/ \_______/ \____/ \__| \____/ \__| \_______|\_______/ \_/ \__| \_______| \_____\____/ ${AnsiColor.BRIGHT_CYAN} :: Application :: ${AnsiColor.BRIGHT_RED}${spring.application.name} ${AnsiColor.BRIGHT_CYAN} :: Developers :: ${AnsiColor.BRIGHT_RED}fordes ${AnsiColor.BRIGHT_CYAN} :: Github :: ${AnsiColor.BRIGHT_RED}https://github.com/fordes123/Subtitles-View${AnsiColor.BRIGHT_WHITE} ================================================ FILE: src/main/resources/css/edit-tool.css ================================================ .toolPanel { -fx-font-size: 15; -fx-text-fill: -fx-dark-0; -fx-background-radius: 5; -fx-border-radius: 5; -fx-background-color: -fx-white-0; -fx-effect: dropshadow(GAUSSIAN, -fx-dark-3, 7, 0, 0, 0); } .dark .toolPanel { -fx-text-fill: -fx-white-0; -fx-background-color: -fx-dark-3; -fx-effect: dropshadow(GAUSSIAN, -fx-dark-0, 7, 0, 0, 0); } .toolPanel .text-field, .toolPanel .label, .toolPanel .button { -fx-text-fill: -fx-dark-0; } .dark .toolPanel .text-field, .dark .toolPanel .label, .dark .toolPanel .button { -fx-text-fill: -fx-white-0 } .menu-bar:hover, .menu:hover, .menu-bar:focused, .menu:focused, .label:hover, .button:hover { -fx-text-fill: -fx-focus-0 !important; -fx-background-color: transparent; } .font-icon { -fx-font-size: 20 !important; } .menu-bar,.menu { -fx-pref-height: 50; -fx-pref-width: 50; -fx-min-height: 50; -fx-min-width: 50; -fx-alignment: center; } .menu, .menu-bar, .check-menu-item { -fx-graphic: true; -fx-background-color: transparent; } .context-menu { -fx-background-color: -fx-white-1; } .dark .context-menu { -fx-background-color: -fx-dark-4; } .left-item { -fx-background-radius: 5 0 0 5; } .right-item { -fx-background-radius: 0 5 5 0; } .left-top-item { -fx-background-radius: 5 0 0 0; } .left-bottom-item { -fx-background-radius: 0 0 0 5; } .right-top-item { -fx-background-radius: 0 5 0 0; } .right-bottom-item { -fx-background-radius: 0 0 5 0; } .error { -fx-text-fill: -fx-error-0 !important; } .dark .error { -fx-text-fill: -fx-error-1 !important; } .jfx-combo-box, .jfx-combo-box > .list-cell { -fx-font-size: 15; -fx-alignment: center; } .input-line, .input-focused-line { -fx-background-color: transparent !important; -fx-pref-height: 0px !important; -fx-translate-y: 0px !important; } ================================================ FILE: src/main/resources/css/font.css ================================================ @font-face { font-family: "iconfont"; src: url('/font/iconfont.ttf') format('truetype'); } @font-face { font-family: "butter sans Rounded"; src: url('/font/buttersans-Rounded.otf') format('truetype'); } ================================================ FILE: src/main/resources/css/main-editor.css ================================================ .bar .item { -fx-font-family: iconfont; -fx-text-fill: -fx-white-5; -fx-text-alignment: center; -fx-padding: 0; -fx-font-size: 20; } .bar .item:selected, .bar .item:hover { -fx-text-fill: -fx-focus-0; } .dark .bar .item:selected, .dark .bar .item:hover { -fx-text-fill: -fx-focus-0; } .dark .bar .item { -fx-text-fill: -fx-white-4; } .text-area { -fx-background-color: -fx-white-0; -fx-text-fill: -fx-dark-0; } .dark .text-area { -fx-background-color: -fx-dark-3; -fx-text-fill: -fx-white-0; } .text-area .scroll-pane .content { -fx-background-radius: 0; -fx-border-radius: 0; -fx-border-insets: 0; -fx-background-insets: 0; } .text-area .scroll-bar, .text-area .track { -fx-max-width: 15; -fx-min-width: 15; } .bottom .toggle-button, .bottom .label { -fx-text-fill: -fx-dark-1; -fx-font-size: 15; } .dark .bottom .label { -fx-text-fill: -fx-white-1; } .bottom .toggle-button .label { -fx-padding: 0; -fx-font-family: iconfont; -fx-font-size: 26; -fx-text-fill: -fx-dark-1; } .bottom .toggle-button:selected .label { -fx-text-fill: -fx-focus-0; } .dark .bottom .toggle-button:selected .label { -fx-text-fill: -fx-focus-1; } .dark #editMode, .dark #editModeIcon{ -fx-text-fill: -fx-white-1; } .styled-text-area { -fx-font-color: -fx-dark-1 !important; -fx-text-fill: -fx-dark-0 !important; -fx-fill: -fx-dark-0 !important; } .dark .styled-text-area { -fx-font-color: -fx-white-0 !important; -fx-text-fill: -fx-white-0 !important; -fx-fill: -fx-white-0 !important; } ================================================ FILE: src/main/resources/css/quick-start.css ================================================ #root { -fx-background-color: transparent; -fx-border-style: dashed; -fx-border-radius: 10; -fx-border-width: 4; -fx-border-color: -fx-white-4; } .error { -fx-border-color: -fx-error-0 !important; } .error #clues, .error .button .label { -fx-text-fill: -fx-error-0c !important; } .dark .error { -fx-border-color: -fx-error-1 !important; } .dark .error #clues, .dark .error .button .label { -fx-text-fill: -fx-error-1 !important; } .warning { -fx-border-color: -fx-wran-0 !important; } .warning #clues, .warning .button .label { -fx-text-fill: -fx-wran-0 !important; } .dark .warning { -fx-border-color: -fx-wran-1 !important; } .dark .warning #clues, .dark .warning .button .label { -fx-text-fill: -fx-wran-1 !important; } .success { -fx-border-color: -fx-success-0 !important; } .success #clues, .success .button .label { -fx-text-fill: -fx-success-0 !important; } .dark .success { -fx-border-color: -fx-success-1 !important; } .dark .success #clues, .dark .success .button .label { -fx-text-fill: -fx-success-1 !important; } .button { -fx-background-color: transparent !important; } .button .label { -fx-font-family: iconfont; -fx-font-size: 80; -fx-text-alignment: center; } .label { -fx-text-fill: -fx-white-4 !important; } .button:hover .label { -fx-text-fill: -fx-focus-0; } .dark .button:hover .label { -fx-text-fill: -fx-focus-1; } .label { -fx-font-size: 22; -fx-text-fill: -fx-white-5; } ================================================ FILE: src/main/resources/css/setting.css ================================================ .item { -fx-font-size: 15; } .sub-title { -fx-font-size: 20; } .item, .sub-title { -fx-text-fill: -fx-dark-0; } .dark .item, .dark .sub-title { -fx-text-fill: -fx-white-0; } .text-field { -fx-font-size: 15; -fx-text-fill: -fx-dark-0; -fx-background-position: 5; -fx-border-radius: 5; -fx-background-color: -fx-white-2; -fx-pref-width: 300; -fx-pref-height: 40; -fx-min-width: -fx-pref-width; -fx-min-height: -fx-pref-height; -fx-max-width: -fx-pref-width; -fx-max-height: -fx-pref-height; } .dark .text-field { -fx-text-fill: -fx-white-0; -fx-background-color: -fx-dark-5; } .text-field :focused { -fx-text-fill: -fx-focus-0; } .dark .text-field :focused { -fx-text-fill: -fx-focus-1; } .popup-text { -fx-font-size: 14; -fx-text-fill: -fx-dark-4 !important; } .popup-text:hover { -fx-text-fill: -fx-focus-0 !important; } .dark .popup-text { -fx-text-fill: -fx-white-4 !important; } .dark .popup-text:hover { -fx-text-fill: -fx-focus-1 !important; } .tips { -fx-border-width: 0; -fx-border-insets: 0; -fx-background-insets: 0; -fx-background-radius: 8; -fx-border-radius: 8; -fx-background-color: -fx-white-2; -fx-line-spacing: 8px; -fx-font-size: 14; } .tips .text { -fx-fill: -fx-focus-0; } .dark .tips .text { -fx-fill: -fx-focus-1; } .dark .tips { -fx-background-color: -fx-dark-5; } .icon { -fx-font-family: iconfont; -fx-font-size: 26 } ================================================ FILE: src/main/resources/css/speech-conversion.css ================================================ ================================================ FILE: src/main/resources/css/styles.css ================================================ * { -fx-white-0: #ffffff; -fx-white-1: #f5f5f5; -fx-white-2: #ebebed; -fx-white-3: #e8e8e8; -fx-white-4: #aaaaaa; -fx-white-5: #4d4d4d; -fx-dark-0: #000000; -fx-dark-1: #101010; -fx-dark-2: #1a1a1a; -fx-dark-3: #212121; -fx-dark-4: #282828; -fx-dark-5: #303030; -fx-focus-0: #5b5bfa; -fx-focus-1: #5b5bfa; -fx-error-0: #e74c3c; -fx-error-1: #c0392b; -fx-wran-0: #f1c40f; -fx-wran-1: #f39c12; -fx-success-0: #2ecc71; -fx-success-1: #27ae60; } .screen { -fx-background-color: transparent; -fx-cursor: hand; } .dark .screen { -fx-effect: dropshadow(gaussian, -fx-dark-4, 8, 0, 0, 0); } .full-screen { -fx-padding: 0; -fx-border-insets: 0; -fx-border-radius: 0; -fx-background-radius: 0; -fx-effect: dropshadow(gaussian, -fx-dark-0, 0, 0, 0, 0); } .normal-screen { -fx-padding: 20 20 20 20; -fx-border-insets: 0.5; -fx-border-radius: 8; -fx-background-radius: 8; -fx-effect: dropshadow(gaussian, -fx-dark-0, 8, 0, 0, 0); } .content { -fx-background-color: -fx-white-0; -fx-background-repeat: repeat; -fx-border-radius: 0 0 8 0; -fx-background-radius: 0 0 8 0; } .content-exclusive { -fx-border-radius: 0 0 8 8; -fx-background-radius: 0 0 8 8; } .dark .content { -fx-background-color: -fx-dark-3; } .sidebar { -fx-padding: 10 0 0 0; -fx-background-color: -fx-white-1; -fx-background-repeat: repeat; -fx-border-radius: 0 0 0 8; -fx-background-radius: 0 0 0 8; } .dark .sidebar { -fx-background-color: -fx-dark-4; } .sidebar-item { -fx-text-fill: -fx-dark-4; -fx-font-size: 14; -fx-end-margin: 6 10 6 10; -fx-start-margin: 6 10 6 10; -fx-background-radius: 10; -fx-background-color: transparent; } .dark .sidebar-item, .dark .sidebar-item:selected, .dark .sidebar-item:hover, .normal-button, .dark .tooltip { -fx-text-fill: -fx-white-0; } .sidebar-item:selected, .sidebar-item:hover { -fx-background-color: -fx-white-3; /*-fx-text-fill: -fx-focus-0;*/ } .dark .sidebar-item:selected, .dark .sidebar-item:hover { -fx-background-color: -fx-dark-5; /*-fx-text-fill: -fx-focus-1;*/ } .sidebar .app-name, .sidebar .logo, .sidebar .setting:hover { -fx-text-fill: -fx-focus-0 !important; } .dark .sidebar .app-name, .dark .sidebar .logo, .dark .sidebar .setting:hover { -fx-text-fill: -fx-focus-1 !important; } .sidebar-icon, .sidebar .setting { -fx-text-alignment: center; -fx-font-family: iconfont !important; -fx-text-fill: -fx-white-5 !important; } .sidebar .logo { -fx-text-alignment: center; -fx-font-family: iconfont !important; } .dark .sidebar-icon, .dark .sidebar .setting { -fx-text-fill: -fx-white-4 !important; } .sidebar-icon { -fx-padding: 0 10 0 0; -fx-font-size: 24 !important; } .sidebar .logo { -fx-font-size: 36 !important; } .sidebar .app-name { -fx-padding: 0 0 0 5; -fx-font-size: 20; -fx-font-family: "butter sans Rounded"; } .sidebar .setting { -fx-background-color: transparent; -fx-font-size: 20; } .normal-button,.dark .normal-button:hover { -jfx-button-type: FLAT; -fx-background-color: -fx-focus-0; } .dark .normal-button, .normal-button:hover { -fx-background-color: -fx-focus-1; } .font-icon { -fx-font-family: iconfont; } .tooltip { -fx-background-color: -fx-white-1; -fx-text-fill: -fx-dark-0; -fx-font-size: 14; } .dark .tooltip { -fx-background-color: -fx-dark-4; } .separator *.line { -fx-border-style: solid; -fx-border-width: 0 0 2 0; /* 宽度 */ -fx-background-color: #E6E6E6; -fx-border-color: #E6E6E6; } .dark .separator *.line { -fx-background-color: #494949; -fx-border-color: #494949 } /*滚动条背景色*/ .scroll-bar, .track { -fx-background-color: transparent; -fx-pref-width: 15; } /*滚动条颜色*/ .thumb { -fx-background-radius: 2; -fx-border-radius: 0; -fx-background-color: -fx-white-3; } .thumb:pressed, .thumb:hover { -fx-background-color: -fx-white-4; } .dark .thumb { -fx-background-color: -fx-dark-4; } .dark .thumb:pressed, .dark .thumb:hover { -fx-background-color: -fx-dark-5; } .separator *.line { -fx-border-style: solid; -fx-border-width: 0 0 2 0; -fx-background-color: -fx-white-3; -fx-border-color: -fx-white-3; } .dark .separator *.line { -fx-background-color: -fx-dark-3; -fx-border-color: -fx-dark-3; } .drawer { -fx-border-insets: 0; -fx-background-insets: 0; -fx-background-radius: 0 5 5 0; -fx-background-color: transparent; -fx-text-fill: transparent; -fx-font-family: iconfont; -fx-font-size: 42; -fx-text-alignment: left; } .drawer:hover { -fx-text-fill: -fx-white-3 !important; } .dark .drawer:hover { -fx-text-fill: -fx-white-5 !important; } .no-border { -fx-background-insets: 0; -fx-border-insets: 0; -fx-border-width: 0; } .transparent { -fx-background-color: transparent; } .scroll-pane .viewport { -fx-background-color: -fx-white-0; } .dark .scroll-pane .viewport { -fx-background-color: -fx-dark-3; } /*ListView*/ .jfx-list-cell-container { -fx-alignment: center-left; } .dark .jfx-list-view, .jfx-list-cell { -fx-background-color: transparent; } .jfx-list-cell, .jfx-list-cell > .jfx-rippler > StackPane { -fx-background-radius: 8; } .jfx-list-cell:selected > .jfx-rippler > StackPane { -fx-background-color: -fx-white-1; } .dark .jfx-list-cell:selected > .jfx-rippler > StackPane { -fx-background-color: -fx-dark-5; } .jfx-list-cell { -fx-background-insets: 0.0; } .jfx-list-cell .jfx-rippler { -jfx-rippler-fill: -fx-white-1; -fx-padding: 0 5 0 0; } .dark .jfx-list-cell .jfx-rippler { -jfx-rippler-fill: -fx-white-5; } .jfx-list-view { -fx-background-insets: 0; -jfx-cell-horizontal-margin: 0.0; -jfx-cell-vertical-margin: 5.0; -jfx-vertical-gap: 10; -jfx-expanded: false; /*-fx-pref-width: 200;*/ } /*RadioButton*/ .jfx-radio-button { -fx-font-size: 15; -fx-text-fill: -fx-dark-0; -jfx-selected-color: -fx-focus-0; -jfx-unselected-color: -fx-white-4; } .dark .jfx-radio-button { -fx-text-fill: -fx-white-0; -jfx-selected-color: -fx-focus-1; -jfx-unselected-color: -fx-white-3; } /*ComboBox*/ .jfx-combo-box { -fx-font-size: 15; -jfx-focus-color: -fx-focus-0; -jfx-unfocus-color: -fx-white-4; -jfx-label-float: false; -fx-text-fill: -fx-dark-0; } .dark .jfx-combo-box { -jfx-focus-color: -fx-focus-1; -jfx-unfocus-color: -fx-white-3; -jfx-label-float: false; -fx-text-fill: -fx-white-0; } .dark .jfx-combo-box .list-cell { -fx-text-fill: -fx-white-0; } .jfx-combo-box .list-view { -fx-background-color: -fx-white-1; -fx-background-radius: 0 0 5 5; } .dark .jfx-combo-box .list-view { -fx-background-color: -fx-dark-4; } .dark .jfx-combo-box .list-view .list-cell { } .jfx-combo-box .list-view .list-cell:filled:selected, .jfx-combo-box .list-view .list-cell:filled:selected:hover { -fx-background-color: -fx-white-1; -fx-text-fill: -fx-focus-0; /*下拉列表字体色*/ } .dark .jfx-combo-box .list-view .list-cell:filled:selected, .dark .jfx-combo-box .list-view .list-cell:filled:selected:hover { -fx-background-color: -fx-dark-4; -fx-text-fill: -fx-focus-1; /*下拉列表字体色*/ } .jfx-combo-box .list-view .list-cell:filled:hover { -fx-background-color: -fx-white-2; -fx-text-fill: -fx-focus-0; } .dark .jfx-combo-box .list-view .list-cell:filled:hover { -fx-background-color: -fx-dark-5; -fx-text-fill: -fx-focus-1; } .dark .jfx-combo-box .jfx-list-cell:selected > .jfx-rippler > StackPane { -fx-background-color: -fx-dark-4; } .jfx-spinner .arc { -fx-stroke-width: 8.0; } ================================================ FILE: src/main/resources/css/subtitle-search.css ================================================ .engine { -fx-background-radius: 50px; -fx-pref-height: 50px; -fx-pref-width: 50px; -fx-min-width: -fx-pref-width; -fx-max-width: -fx-pref-width; -fx-min-height: -fx-pref-height; -fx-max-height: -fx-pref-height; -fx-background-color: -fx-white-1; -fx-font-family: iconfont; -fx-text-fill: -fx-white-5; -fx-font-size: 20; -fx-text-alignment: center; -fx-effect: dropshadow(gaussian, -fx-white-4, 10, 0, 0, 0); } .engine:selected, .engine:hover { -fx-text-fill: -fx-focus-0; } .dark .engine:selected, .dark .engine:hover { -fx-text-fill: -fx-focus-1; } .dark .engine { -fx-background-color: -fx-dark-5; -fx-text-fill: -fx-white-4; -fx-effect: dropshadow(gaussian, -fx-dark-2, 10, 0, 0, 0); } .jfx-button { -jfx-button-type: RAISED; } .list-cell .label { -fx-text-fill: -fx-dark-0; } .dark .list-cell .label, .dark #searchField { -fx-text-fill: -fx-white-0; } .list-cell:selected .label { -fx-text-fill: -fx-focus-0; } .dark .list-cell:selected .label { -fx-text-fill: -fx-focus-1; } .list-cell .caption { -fx-font-size: 14; } .search-item { -fx-pref-width: 200; -fx-pref-height: 40; -fx-min-width: -fx-pref-width; -fx-min-height: -fx-pref-height; } #searchField { -fx-font-size: 14; -fx-prompt-text-fill: -fx-white-5; } ================================================ FILE: src/main/resources/css/title-bar.css ================================================ #root { -fx-background-color: -fx-white-3; } .dark #root { -fx-background-color: -fx-dark-2; } .full-screen #root { -fx-border-radius: 0; -fx-background-radius: 0; } .normal-screen #root { -fx-border-radius: 8 8 0 0; -fx-background-radius: 8 8 0 0; } #closed:hover { -fx-background-color: red; -fx-text-fill: -fx-white-3; } .dark #closed:hover { -fx-background-color: red; } .normal-screen #closed { -fx-background-radius: 0 8 0 0; } .full-screen #closed { -fx-background-radius: 0 0 0 0; } #minimize:hover, #maximize:hover { -fx-background-color: -fx-white-3; -fx-background-radius: 0; } .dark #minimize:hover, .dark #maximize:hover { -fx-background-color: -fx-dark-3; -fx-background-radius: 0; } .title-button { -fx-pref-height: 30; -fx-pref-width: 50; -fx-background-color: transparent; -fx-font-family: iconfont; -fx-text-fill: -fx-white-5; -fx-font-size: 20; -fx-text-alignment: center; } ================================================ FILE: src/main/resources/css/toast.css ================================================ .toast { -fx-cursor: hand; -fx-background-color: -fx-white-0; -fx-border-insets: 0; -fx-background-radius: 10; -fx-padding: 10 10 10 10; -fx-effect: dropshadow(gaussian, -fx-white-4, 10, 0, 0, 0); } .dark .toast { -fx-background-color: -fx-dark-3; -fx-effect: dropshadow(gaussian, -fx-dark-1, 10, 0, 0, 0); } #perform { -fx-pref-height: 30; -fx-pref-width: 70; -fx-background-radius: 5; -fx-font-size: 12; } .choose { -fx-pref-height: 20; -fx-pref-width: 40; -fx-background-radius: 5; -fx-font-size: 10; } #perform:hover { -fx-background-color: -fx-focus-0; -fx-effect: dropshadow(gaussian, -fx-white-4, 5, 0, 0, 0); } .dark #perform { -fx-background-color: -fx-focus-1; } .dark #perform:hover { -fx-background-color: -fx-focus-1; -fx-effect: dropshadow(gaussian, -fx-dark-2, 5, 0, 0, 0); } #_caption { -fx-font-size: 14; -fx-text-fill: -fx-dark-0 !important; } .dark #_caption { -fx-text-fill: -fx-white-1 !important; } #_text { -fx-font-size: 13; -fx-text-fill: -fx-white-5 !important; } .dark #_text { -fx-text-fill: -fx-white-4 !important; } ================================================ FILE: src/main/resources/css/tool-box.css ================================================ ================================================ FILE: src/main/resources/fxml/edit-tool.fxml ================================================ ================================================ FILE: src/main/resources/fxml/export.fxml ================================================ ================================================ FILE: src/main/resources/fxml/main-editor.fxml ================================================ ================================================ FILE: src/main/resources/fxml/main-view.fxml ================================================ ================================================ FILE: src/main/resources/fxml/quick-start.fxml ================================================ ================================================ FILE: src/main/resources/fxml/setting.fxml ================================================ ================================================ FILE: src/main/resources/fxml/sidebar-after.fxml ================================================ ================================================ FILE: src/main/resources/fxml/sidebar-before.fxml ================================================ ================================================ FILE: src/main/resources/fxml/sidebar-bottom.fxml ================================================ ================================================ FILE: src/main/resources/fxml/speech-conversion.fxml ================================================ ================================================ FILE: src/main/resources/fxml/subtitle-search.fxml ================================================ ================================================ FILE: src/main/resources/fxml/sync-editor.fxml ================================================ ================================================ FILE: src/main/resources/fxml/title-bar.fxml ================================================ ================================================ FILE: src/main/resources/fxml/toast.fxml ================================================ ================================================ FILE: src/main/resources/fxml/tool-box.fxml ================================================ ================================================ FILE: src/main/resources/fxml/voice-convert.fxml ================================================ ================================================ FILE: src/main/resources/logback/logback-spring.xml ================================================ INFO %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} ${LOG_HOME}/${appName}.log ${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log 365 5MB %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} ================================================ FILE: src/main/resources/mapper/InterfaceMapper.xml ================================================