diff --git a/.github/workflows/subtitles-view.yml b/.github/workflows/subtitles-view.yml new file mode 100644 index 0000000..314f793 --- /dev/null +++ b/.github/workflows/subtitles-view.yml @@ -0,0 +1,92 @@ +# 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..762a045 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +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/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ba2b3f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c95cc31 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# 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) 许可进行开源。 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5eb3a96 --- /dev/null +++ b/pom.xml @@ -0,0 +1,216 @@ + + + 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 + + + + + + + + diff --git a/screenshot/action.png b/screenshot/action.png new file mode 100644 index 0000000..5d37e2b Binary files /dev/null and b/screenshot/action.png differ diff --git a/screenshot/home.png b/screenshot/home.png new file mode 100644 index 0000000..b83ce32 Binary files /dev/null and b/screenshot/home.png differ diff --git a/src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java b/src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java new file mode 100644 index 0000000..9a117c4 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/SubtitlesViewApplication.java @@ -0,0 +1,109 @@ +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()); + } + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java b/src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java new file mode 100644 index 0000000..945d831 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/config/ApplicationConfig.java @@ -0,0 +1,76 @@ +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)); + } +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java b/src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java new file mode 100644 index 0000000..e709900 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/config/ExecutorConfig.java @@ -0,0 +1,27 @@ +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(); + } + + +} diff --git a/src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java b/src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java new file mode 100644 index 0000000..4f82320 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/constant/CommonConstant.java @@ -0,0 +1,50 @@ +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"; +} diff --git a/src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java b/src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java new file mode 100644 index 0000000..cd7b971 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/constant/StyleClassConstant.java @@ -0,0 +1,33 @@ +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"; +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java b/src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java new file mode 100644 index 0000000..8e73098 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/DelayInitController.java @@ -0,0 +1,54 @@ +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() {}; +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/EditTool.java b/src/main/java/org/fordes/subtitles/view/controller/EditTool.java new file mode 100644 index 0000000..dab1e5d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/EditTool.java @@ -0,0 +1,475 @@ +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; + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/Export.java b/src/main/java/org/fordes/subtitles/view/controller/Export.java new file mode 100644 index 0000000..2b43f21 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/Export.java @@ -0,0 +1,12 @@ +package org.fordes.subtitles.view.controller; + +import org.springframework.stereotype.Component; + +/** + * 语音转换 控制器 + * + * @author fordes on 2022/4/8 + */ +@Component +public class Export { +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/MainController.java b/src/main/java/org/fordes/subtitles/view/controller/MainController.java new file mode 100644 index 0000000..a20cc40 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/MainController.java @@ -0,0 +1,206 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/MainEditor.java b/src/main/java/org/fordes/subtitles/view/controller/MainEditor.java new file mode 100644 index 0000000..ffd0c54 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/MainEditor.java @@ -0,0 +1,208 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/QuickStart.java b/src/main/java/org/fordes/subtitles/view/controller/QuickStart.java new file mode 100644 index 0000000..82cb438 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/QuickStart.java @@ -0,0 +1,98 @@ +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(); + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/Setting.java b/src/main/java/org/fordes/subtitles/view/controller/Setting.java new file mode 100644 index 0000000..0623226 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/Setting.java @@ -0,0 +1,270 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java b/src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java new file mode 100644 index 0000000..dceb7b2 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/SidebarAfter.java @@ -0,0 +1,22 @@ +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; +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java b/src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java new file mode 100644 index 0000000..729045c --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/SidebarBefore.java @@ -0,0 +1,22 @@ +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; +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java b/src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java new file mode 100644 index 0000000..813027b --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/SidebarBottom.java @@ -0,0 +1,21 @@ +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; + + +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java b/src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java new file mode 100644 index 0000000..81b95d5 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/SpeechConversion.java @@ -0,0 +1,12 @@ +package org.fordes.subtitles.view.controller; + +import org.springframework.stereotype.Component; + +/** + * 语音转换 控制器 + * + * @author fordes on 2022/4/8 + */ +@Component +public class SpeechConversion { +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java b/src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java new file mode 100644 index 0000000..347b496 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/SubtitleSearch.java @@ -0,0 +1,188 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java b/src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java new file mode 100644 index 0000000..382be9e --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/SyncEditor.java @@ -0,0 +1,12 @@ +package org.fordes.subtitles.view.controller; + +import org.springframework.stereotype.Component; + +/** + * 语音转换 控制器 + * + * @author fordes on 2022/4/8 + */ +@Component +public class SyncEditor { +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/TitleBar.java b/src/main/java/org/fordes/subtitles/view/controller/TitleBar.java new file mode 100644 index 0000000..fd79941 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/TitleBar.java @@ -0,0 +1,54 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/Toast.java b/src/main/java/org/fordes/subtitles/view/controller/Toast.java new file mode 100644 index 0000000..3db28b0 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/Toast.java @@ -0,0 +1,80 @@ +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); + } + }); + } + + +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/ToolBox.java b/src/main/java/org/fordes/subtitles/view/controller/ToolBox.java new file mode 100644 index 0000000..f1a8881 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/ToolBox.java @@ -0,0 +1,10 @@ +package org.fordes.subtitles.view.controller; + +import org.springframework.stereotype.Component; + +/** + * @author fordes on 2022/2/6 + */ +@Component +public class ToolBox { +} diff --git a/src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java b/src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java new file mode 100644 index 0000000..7900624 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/controller/VoiceConvert.java @@ -0,0 +1,36 @@ +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); + } + }); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java b/src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java new file mode 100644 index 0000000..c4e444a --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/enums/EditToolEventEnum.java @@ -0,0 +1,18 @@ +package org.fordes.subtitles.view.enums; + +/** + * 编辑工具 事件类型枚举 + * + * @author fordes on 2022/7/15 + */ +public enum EditToolEventEnum { + + SEARCH, //搜索 + REPLACE,//替换 + JUMP,//跳转 + FONT, //字体(样式) + TIMELINE, //时间轴 + CODE,//编码 + REF, //刷新 + TRANSLATE //翻译 +} diff --git a/src/main/java/org/fordes/subtitles/view/enums/FileEnum.java b/src/main/java/org/fordes/subtitles/view/enums/FileEnum.java new file mode 100644 index 0000000..7d48090 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/enums/FileEnum.java @@ -0,0 +1,72 @@ +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; + } +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/enums/FontIcon.java b/src/main/java/org/fordes/subtitles/view/enums/FontIcon.java new file mode 100644 index 0000000..5e89436 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/enums/FontIcon.java @@ -0,0 +1,64 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java b/src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java new file mode 100644 index 0000000..ffab333 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/enums/ServiceProvider.java @@ -0,0 +1,29 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/enums/ServiceType.java b/src/main/java/org/fordes/subtitles/view/enums/ServiceType.java new file mode 100644 index 0000000..f6b2914 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/enums/ServiceType.java @@ -0,0 +1,30 @@ +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(); + } +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java b/src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java new file mode 100644 index 0000000..ebef518 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/enums/SevenZipEnum.java @@ -0,0 +1,48 @@ +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; + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java b/src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java new file mode 100644 index 0000000..6caf9f6 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/AbstractToastEvent.java @@ -0,0 +1,23 @@ +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); +} diff --git a/src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java b/src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java new file mode 100644 index 0000000..4693c95 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/EditToolEvent.java @@ -0,0 +1,45 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java b/src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java new file mode 100644 index 0000000..032df2e --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/FileOpenEvent.java @@ -0,0 +1,42 @@ +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("出错了","打开文件失败!")); + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java b/src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java new file mode 100644 index 0000000..1aa8665 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/LoadingEvent.java @@ -0,0 +1,28 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java b/src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java new file mode 100644 index 0000000..60f7ce3 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/ThemeChangeEvent.java @@ -0,0 +1,25 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java b/src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java new file mode 100644 index 0000000..7ed508d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/ToastChooseEvent.java @@ -0,0 +1,52 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java b/src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java new file mode 100644 index 0000000..a85ec3b --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/ToastConfirmEvent.java @@ -0,0 +1,42 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java b/src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java new file mode 100644 index 0000000..00833f5 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/event/TranslateEvent.java @@ -0,0 +1,35 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java b/src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java new file mode 100644 index 0000000..4eb8652 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/factory/TranslateServiceFactory.java @@ -0,0 +1,22 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java b/src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java new file mode 100644 index 0000000..3dde82d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/handler/CallBackHandler.java @@ -0,0 +1,10 @@ +package org.fordes.subtitles.view.handler; + +/** + * @author fordes on 2022/7/27 + */ +@FunctionalInterface +public interface CallBackHandler { + + void handle(T value); +} diff --git a/src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java b/src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java new file mode 100644 index 0000000..320edb0 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/handler/EditToolEventHandler.java @@ -0,0 +1,12 @@ +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 { +} diff --git a/src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java b/src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java new file mode 100644 index 0000000..ad5ee46 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/handler/FileOpenEventHandler.java @@ -0,0 +1,13 @@ +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 = "文件打开失败"; + +} diff --git a/src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java b/src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java new file mode 100644 index 0000000..919673f --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/handler/ToastEventHandler.java @@ -0,0 +1,37 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java b/src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java new file mode 100644 index 0000000..e065dbb --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/handler/ToastHandler.java @@ -0,0 +1,12 @@ +package org.fordes.subtitles.view.handler; + +/** + * toast回调事件处理器接口 + * + * @author fordes on 2022/2/2 + */ +@FunctionalInterface +public interface ToastHandler { + + void handle(); +} diff --git a/src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java b/src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java new file mode 100644 index 0000000..03fa647 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/mapper/InterfaceMapper.java @@ -0,0 +1,25 @@ +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(); +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java b/src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java new file mode 100644 index 0000000..4819401 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/mapper/LanguageMapper.java @@ -0,0 +1,10 @@ +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 { + +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java b/src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java new file mode 100644 index 0000000..d6c0f60 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/mapper/SearchCasesMapper.java @@ -0,0 +1,12 @@ +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 { +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java b/src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java new file mode 100644 index 0000000..fd0bb7d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/mapper/VersionMapper.java @@ -0,0 +1,12 @@ +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 { +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java b/src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java new file mode 100644 index 0000000..3a0d444 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/DTO/AvailableServiceInfo.java @@ -0,0 +1,21 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java b/src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java new file mode 100644 index 0000000..63f21de --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/DTO/Subtitle.java @@ -0,0 +1,22 @@ +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; +} diff --git a/src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java b/src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java new file mode 100644 index 0000000..17516e3 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/DTO/TranslateResult.java @@ -0,0 +1,20 @@ +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; +} diff --git a/src/main/java/org/fordes/subtitles/view/model/DTO/Video.java b/src/main/java/org/fordes/subtitles/view/model/DTO/Video.java new file mode 100644 index 0000000..fbdc849 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/DTO/Video.java @@ -0,0 +1,29 @@ +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; +} diff --git a/src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java b/src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java new file mode 100644 index 0000000..72bbf98 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/PO/FileRecord.java @@ -0,0 +1,72 @@ +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; + +} diff --git a/src/main/java/org/fordes/subtitles/view/model/PO/Interface.java b/src/main/java/org/fordes/subtitles/view/model/PO/Interface.java new file mode 100644 index 0000000..441e261 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/PO/Interface.java @@ -0,0 +1,33 @@ +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; +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/model/PO/Language.java b/src/main/java/org/fordes/subtitles/view/model/PO/Language.java new file mode 100644 index 0000000..306477c --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/PO/Language.java @@ -0,0 +1,56 @@ +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"; +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java b/src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java new file mode 100644 index 0000000..f368297 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/PO/SearchCases.java @@ -0,0 +1,44 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java b/src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java new file mode 100644 index 0000000..d4831fa --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/PO/ServiceInterface.java @@ -0,0 +1,58 @@ +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; +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/model/PO/Version.java b/src/main/java/org/fordes/subtitles/view/model/PO/Version.java new file mode 100644 index 0000000..11a6f52 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/PO/Version.java @@ -0,0 +1,46 @@ +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; +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/model/search/Cases.java b/src/main/java/org/fordes/subtitles/view/model/search/Cases.java new file mode 100644 index 0000000..6cc5b66 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/search/Cases.java @@ -0,0 +1,34 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/model/search/Engine.java b/src/main/java/org/fordes/subtitles/view/model/search/Engine.java new file mode 100644 index 0000000..6e3ddfd --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/search/Engine.java @@ -0,0 +1,20 @@ +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; +} diff --git a/src/main/java/org/fordes/subtitles/view/model/search/Result.java b/src/main/java/org/fordes/subtitles/view/model/search/Result.java new file mode 100644 index 0000000..ff3da0a --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/search/Result.java @@ -0,0 +1,43 @@ +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() + } +} diff --git a/src/main/java/org/fordes/subtitles/view/model/search/Selector.java b/src/main/java/org/fordes/subtitles/view/model/search/Selector.java new file mode 100644 index 0000000..929c19c --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/model/search/Selector.java @@ -0,0 +1,44 @@ +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; + +} diff --git a/src/main/java/org/fordes/subtitles/view/service/ConfigService.java b/src/main/java/org/fordes/subtitles/view/service/ConfigService.java new file mode 100644 index 0000000..005c324 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/ConfigService.java @@ -0,0 +1,12 @@ +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 { + + +} diff --git a/src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java b/src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java new file mode 100644 index 0000000..c22ebed --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/Impl/InterfaceServiceImpl.java @@ -0,0 +1,52 @@ +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()); + } +} + diff --git a/src/main/java/org/fordes/subtitles/view/service/InterfaceService.java b/src/main/java/org/fordes/subtitles/view/service/InterfaceService.java new file mode 100644 index 0000000..a699e18 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/InterfaceService.java @@ -0,0 +1,39 @@ +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); +} diff --git a/src/main/java/org/fordes/subtitles/view/service/SearchService.java b/src/main/java/org/fordes/subtitles/view/service/SearchService.java new file mode 100644 index 0000000..5c8a556 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/SearchService.java @@ -0,0 +1,145 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java b/src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java new file mode 100644 index 0000000..6af9621 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/AliTranslateService.java @@ -0,0 +1,42 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java b/src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java new file mode 100644 index 0000000..e951f8f --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/BaiduTranslateService.java @@ -0,0 +1,37 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java b/src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java new file mode 100644 index 0000000..c71d662 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/HuoShanTranslateService.java @@ -0,0 +1,44 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java b/src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java new file mode 100644 index 0000000..fe0e672 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/TencentTranslateService.java @@ -0,0 +1,43 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java b/src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java new file mode 100644 index 0000000..6909afa --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/TranslateService.java @@ -0,0 +1,139 @@ +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); + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java b/src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java new file mode 100644 index 0000000..ebb7f2d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/thread/AliTranslateThread.java @@ -0,0 +1,113 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java b/src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java new file mode 100644 index 0000000..b3e632e --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/thread/BaiduTranslateThread.java @@ -0,0 +1,66 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java b/src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java new file mode 100644 index 0000000..117ec17 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/thread/HuoShanTranslateThread.java @@ -0,0 +1,150 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java b/src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java new file mode 100644 index 0000000..5d24884 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/thread/TencentTranslateThread.java @@ -0,0 +1,129 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java b/src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java new file mode 100644 index 0000000..b825489 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/service/translate/thread/TranslateThread.java @@ -0,0 +1,37 @@ +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; +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java b/src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java new file mode 100644 index 0000000..137e482 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/ArchiveUtil.java @@ -0,0 +1,124 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java b/src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java new file mode 100644 index 0000000..1d38ed7 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/CacheUtil.java @@ -0,0 +1,84 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/FileUtils.java b/src/main/java/org/fordes/subtitles/view/utils/FileUtils.java new file mode 100644 index 0000000..59a966b --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/FileUtils.java @@ -0,0 +1,150 @@ +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("写入文件出错"); + } + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java b/src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java new file mode 100644 index 0000000..3979582 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/SubtitleUtil.java @@ -0,0 +1,294 @@ +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(); + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java b/src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java new file mode 100644 index 0000000..640210b --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/TranslateUtil.java @@ -0,0 +1,133 @@ +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(); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java b/src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java new file mode 100644 index 0000000..18fe14e --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/search/HTMLParsing.java @@ -0,0 +1,54 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java b/src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java new file mode 100644 index 0000000..91d6e97 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/search/JSONParsing.java @@ -0,0 +1,38 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java b/src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java new file mode 100644 index 0000000..ddc63dc --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/search/Parsing.java @@ -0,0 +1,17 @@ +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); +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/search/ParsingFactory.java b/src/main/java/org/fordes/subtitles/view/utils/search/ParsingFactory.java new file mode 100644 index 0000000..327f22e --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/search/ParsingFactory.java @@ -0,0 +1,23 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/SubmergeAPI.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/SubmergeAPI.java new file mode 100644 index 0000000..8aa82c2 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/SubmergeAPI.java @@ -0,0 +1,208 @@ +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()); + } + } + } + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/TimedLinesAPI.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/TimedLinesAPI.java new file mode 100644 index 0000000..3849cfb --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/TimedLinesAPI.java @@ -0,0 +1,161 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/constant/FontName.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/constant/FontName.java new file mode 100644 index 0000000..9395f7d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/constant/FontName.java @@ -0,0 +1,53 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ASSParser.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ASSParser.java new file mode 100644 index 0000000..9ebe859 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ASSParser.java @@ -0,0 +1,396 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/BaseParser.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/BaseParser.java new file mode 100644 index 0000000..16af95f --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/BaseParser.java @@ -0,0 +1,158 @@ +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); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/LRCParser.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/LRCParser.java new file mode 100644 index 0000000..e63a1f2 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/LRCParser.java @@ -0,0 +1,66 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ParserFactory.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ParserFactory.java new file mode 100644 index 0000000..2b1f795 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/ParserFactory.java @@ -0,0 +1,35 @@ +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(); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SRTParser.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SRTParser.java new file mode 100644 index 0000000..bfcc145 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SRTParser.java @@ -0,0 +1,123 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SubtitleParser.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SubtitleParser.java new file mode 100644 index 0000000..f38ad98 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/SubtitleParser.java @@ -0,0 +1,69 @@ +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); +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidAssSubException.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidAssSubException.java new file mode 100644 index 0000000..fda1b69 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidAssSubException.java @@ -0,0 +1,27 @@ +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); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidColorCode.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidColorCode.java new file mode 100644 index 0000000..d87fe89 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidColorCode.java @@ -0,0 +1,26 @@ +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); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidFileException.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidFileException.java new file mode 100644 index 0000000..d42c145 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidFileException.java @@ -0,0 +1,26 @@ +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); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSRTSubException.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSRTSubException.java new file mode 100644 index 0000000..f6e66fe --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSRTSubException.java @@ -0,0 +1,27 @@ +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); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSubException.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSubException.java new file mode 100644 index 0000000..cc41875 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/parser/exception/InvalidSubException.java @@ -0,0 +1,25 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSSub.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSSub.java new file mode 100644 index 0000000..7e03ee9 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSSub.java @@ -0,0 +1,125 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSTime.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSTime.java new file mode 100644 index 0000000..bf1e143 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ASSTime.java @@ -0,0 +1,67 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/Events.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/Events.java new file mode 100644 index 0000000..3262589 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/Events.java @@ -0,0 +1,154 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ScriptInfo.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ScriptInfo.java new file mode 100644 index 0000000..02e951a --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/ScriptInfo.java @@ -0,0 +1,284 @@ +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); + } + } + + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/V4Style.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/V4Style.java new file mode 100644 index 0000000..f8c1e5a --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/ass/V4Style.java @@ -0,0 +1,259 @@ +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(); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleLine.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleLine.java new file mode 100644 index 0000000..7b600d9 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleLine.java @@ -0,0 +1,107 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleTime.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleTime.java new file mode 100644 index 0000000..a0bc00a --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/SubtitleTime.java @@ -0,0 +1,84 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedLine.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedLine.java new file mode 100644 index 0000000..e7d2f80 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedLine.java @@ -0,0 +1,33 @@ +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(); + +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedObject.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedObject.java new file mode 100644 index 0000000..1be525d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedObject.java @@ -0,0 +1,42 @@ +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); +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedTextFile.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedTextFile.java new file mode 100644 index 0000000..1c88ef5 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/common/TimedTextFile.java @@ -0,0 +1,33 @@ +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(); + +} \ No newline at end of file diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/Font.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/Font.java new file mode 100644 index 0000000..234b0a0 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/Font.java @@ -0,0 +1,42 @@ +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; + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/SimpleSubConfig.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/SimpleSubConfig.java new file mode 100644 index 0000000..b9cec5d --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/config/SimpleSubConfig.java @@ -0,0 +1,29 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCLine.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCLine.java new file mode 100644 index 0000000..ae7f8d4 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCLine.java @@ -0,0 +1,30 @@ +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(); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCSub.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCSub.java new file mode 100644 index 0000000..4b26542 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCSub.java @@ -0,0 +1,42 @@ +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; + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCTime.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCTime.java new file mode 100644 index 0000000..c53f47c --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/lrc/LRCTime.java @@ -0,0 +1,55 @@ +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; + } + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTLine.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTLine.java new file mode 100644 index 0000000..21af708 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTLine.java @@ -0,0 +1,41 @@ +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(); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTSub.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTSub.java new file mode 100644 index 0000000..03c959a --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTSub.java @@ -0,0 +1,67 @@ +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; + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTTime.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTTime.java new file mode 100644 index 0000000..2cc2185 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/subtitle/srt/SRTTime.java @@ -0,0 +1,69 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ColorUtils.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ColorUtils.java new file mode 100644 index 0000000..5480620 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ColorUtils.java @@ -0,0 +1,83 @@ +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)); + } + +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ConvertUtils.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ConvertUtils.java new file mode 100644 index 0000000..f7f5f28 --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/ConvertUtils.java @@ -0,0 +1,91 @@ +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); + } +} diff --git a/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/EncodeUtils.java b/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/EncodeUtils.java new file mode 100644 index 0000000..79423ad --- /dev/null +++ b/src/main/java/org/fordes/subtitles/view/utils/submerge/utils/EncodeUtils.java @@ -0,0 +1,70 @@ +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; + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ab3ae0c --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,42 @@ +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 \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..d11e408 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,13 @@ + +${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} diff --git a/src/main/resources/css/edit-tool.css b/src/main/resources/css/edit-tool.css new file mode 100644 index 0000000..6d833a3 --- /dev/null +++ b/src/main/resources/css/edit-tool.css @@ -0,0 +1,108 @@ +.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; +} diff --git a/src/main/resources/css/font.css b/src/main/resources/css/font.css new file mode 100644 index 0000000..2611e87 --- /dev/null +++ b/src/main/resources/css/font.css @@ -0,0 +1,9 @@ +@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'); +} \ No newline at end of file diff --git a/src/main/resources/css/main-editor.css b/src/main/resources/css/main-editor.css new file mode 100644 index 0000000..e1beb42 --- /dev/null +++ b/src/main/resources/css/main-editor.css @@ -0,0 +1,86 @@ +.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; +} diff --git a/src/main/resources/css/quick-start.css b/src/main/resources/css/quick-start.css new file mode 100644 index 0000000..44708f8 --- /dev/null +++ b/src/main/resources/css/quick-start.css @@ -0,0 +1,83 @@ +#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; +} + diff --git a/src/main/resources/css/setting.css b/src/main/resources/css/setting.css new file mode 100644 index 0000000..026c552 --- /dev/null +++ b/src/main/resources/css/setting.css @@ -0,0 +1,88 @@ +.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 +} + diff --git a/src/main/resources/css/speech-conversion.css b/src/main/resources/css/speech-conversion.css new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/css/styles.css b/src/main/resources/css/styles.css new file mode 100644 index 0000000..0af8d5a --- /dev/null +++ b/src/main/resources/css/styles.css @@ -0,0 +1,399 @@ +* { + -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; +} + + diff --git a/src/main/resources/css/subtitle-search.css b/src/main/resources/css/subtitle-search.css new file mode 100644 index 0000000..2bf9fba --- /dev/null +++ b/src/main/resources/css/subtitle-search.css @@ -0,0 +1,67 @@ +.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; +} \ No newline at end of file diff --git a/src/main/resources/css/title-bar.css b/src/main/resources/css/title-bar.css new file mode 100644 index 0000000..1f87dc7 --- /dev/null +++ b/src/main/resources/css/title-bar.css @@ -0,0 +1,56 @@ +#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; +} diff --git a/src/main/resources/css/toast.css b/src/main/resources/css/toast.css new file mode 100644 index 0000000..a1405aa --- /dev/null +++ b/src/main/resources/css/toast.css @@ -0,0 +1,60 @@ +.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; +} \ No newline at end of file diff --git a/src/main/resources/css/tool-box.css b/src/main/resources/css/tool-box.css new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/db/subtitles-view.sqlite b/src/main/resources/db/subtitles-view.sqlite new file mode 100644 index 0000000..fb255b8 Binary files /dev/null and b/src/main/resources/db/subtitles-view.sqlite differ diff --git a/src/main/resources/font/buttersans-Rounded.otf b/src/main/resources/font/buttersans-Rounded.otf new file mode 100644 index 0000000..828eeaf Binary files /dev/null and b/src/main/resources/font/buttersans-Rounded.otf differ diff --git a/src/main/resources/font/iconfont.ttf b/src/main/resources/font/iconfont.ttf new file mode 100644 index 0000000..79bbecb Binary files /dev/null and b/src/main/resources/font/iconfont.ttf differ diff --git a/src/main/resources/fxml/edit-tool.fxml b/src/main/resources/fxml/edit-tool.fxml new file mode 100644 index 0000000..30e14ea --- /dev/null +++ b/src/main/resources/fxml/edit-tool.fxml @@ -0,0 +1,463 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/export.fxml b/src/main/resources/fxml/export.fxml new file mode 100644 index 0000000..95640eb --- /dev/null +++ b/src/main/resources/fxml/export.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/src/main/resources/fxml/main-editor.fxml b/src/main/resources/fxml/main-editor.fxml new file mode 100644 index 0000000..a949e46 --- /dev/null +++ b/src/main/resources/fxml/main-editor.fxml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/main-view.fxml b/src/main/resources/fxml/main-view.fxml new file mode 100644 index 0000000..695cae5 --- /dev/null +++ b/src/main/resources/fxml/main-view.fxml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/quick-start.fxml b/src/main/resources/fxml/quick-start.fxml new file mode 100644 index 0000000..0cf2742 --- /dev/null +++ b/src/main/resources/fxml/quick-start.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/setting.fxml b/src/main/resources/fxml/setting.fxml new file mode 100644 index 0000000..e8a863c --- /dev/null +++ b/src/main/resources/fxml/setting.fxml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/sidebar-after.fxml b/src/main/resources/fxml/sidebar-after.fxml new file mode 100644 index 0000000..a1c5a42 --- /dev/null +++ b/src/main/resources/fxml/sidebar-after.fxml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/sidebar-before.fxml b/src/main/resources/fxml/sidebar-before.fxml new file mode 100644 index 0000000..05052ea --- /dev/null +++ b/src/main/resources/fxml/sidebar-before.fxml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/sidebar-bottom.fxml b/src/main/resources/fxml/sidebar-bottom.fxml new file mode 100644 index 0000000..fab5a98 --- /dev/null +++ b/src/main/resources/fxml/sidebar-bottom.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/speech-conversion.fxml b/src/main/resources/fxml/speech-conversion.fxml new file mode 100644 index 0000000..1f882e1 --- /dev/null +++ b/src/main/resources/fxml/speech-conversion.fxml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/main/resources/fxml/subtitle-search.fxml b/src/main/resources/fxml/subtitle-search.fxml new file mode 100644 index 0000000..015b0c4 --- /dev/null +++ b/src/main/resources/fxml/subtitle-search.fxml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/sync-editor.fxml b/src/main/resources/fxml/sync-editor.fxml new file mode 100644 index 0000000..52b45a1 --- /dev/null +++ b/src/main/resources/fxml/sync-editor.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/src/main/resources/fxml/title-bar.fxml b/src/main/resources/fxml/title-bar.fxml new file mode 100644 index 0000000..2f1c886 --- /dev/null +++ b/src/main/resources/fxml/title-bar.fxml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/toast.fxml b/src/main/resources/fxml/toast.fxml new file mode 100644 index 0000000..4c6808f --- /dev/null +++ b/src/main/resources/fxml/toast.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/tool-box.fxml b/src/main/resources/fxml/tool-box.fxml new file mode 100644 index 0000000..1fee8e8 --- /dev/null +++ b/src/main/resources/fxml/tool-box.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/voice-convert.fxml b/src/main/resources/fxml/voice-convert.fxml new file mode 100644 index 0000000..fb9ce91 --- /dev/null +++ b/src/main/resources/fxml/voice-convert.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/src/main/resources/icon/logo.ico b/src/main/resources/icon/logo.ico new file mode 100644 index 0000000..14b0225 Binary files /dev/null and b/src/main/resources/icon/logo.ico differ diff --git a/src/main/resources/icon/logo.png b/src/main/resources/icon/logo.png new file mode 100644 index 0000000..1c96974 Binary files /dev/null and b/src/main/resources/icon/logo.png differ diff --git a/src/main/resources/logback/logback-spring.xml b/src/main/resources/logback/logback-spring.xml new file mode 100644 index 0000000..4d215c9 --- /dev/null +++ b/src/main/resources/logback/logback-spring.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + 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} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/InterfaceMapper.xml b/src/main/resources/mapper/InterfaceMapper.xml new file mode 100644 index 0000000..51e379a --- /dev/null +++ b/src/main/resources/mapper/InterfaceMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/fordes/subtitles/view/SubtitlesViewApplicationTests.java b/src/test/java/org/fordes/subtitles/view/SubtitlesViewApplicationTests.java new file mode 100644 index 0000000..8156677 --- /dev/null +++ b/src/test/java/org/fordes/subtitles/view/SubtitlesViewApplicationTests.java @@ -0,0 +1,17 @@ +package org.fordes.subtitles.view; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; + +@Slf4j +@SpringBootTest +class SubtitlesViewApplicationTests { + + @Test + void contextLoads() throws IOException { + } + +}