├── icon.png ├── src └── main │ ├── resources │ ├── config.properties │ ├── images │ │ ├── a-z.png │ │ ├── z-a.png │ │ ├── close.png │ │ ├── icon.png │ │ ├── remove.png │ │ ├── reset.png │ │ ├── search.png │ │ ├── question.png │ │ ├── replace.png │ │ ├── clockwise.png │ │ ├── default_cover.png │ │ └── counterclockwise.png │ └── draft_templates │ │ ├── draft_track.json │ │ ├── draft_extra_material.json │ │ ├── draft_subtitle_content.json │ │ ├── draft_segment.json │ │ └── draft_text.json │ └── java │ └── app │ └── jackychu │ └── jysrttools │ ├── ui │ ├── DraftListModel.java │ ├── AboutPanel.java │ ├── JyTextPanel.java │ ├── DraftListCellRender.java │ ├── SubtitleCellEditor.java │ ├── ErrorMessagePanel.java │ ├── SubtitleCellRender.java │ ├── DraftSubtitleTableModel.java │ ├── TranslateProgressDialog.java │ ├── JyMenuBar.java │ ├── DraftTextsPanel.java │ ├── SearchBox.java │ ├── DraftListPanel.java │ ├── FindReplaceDialog.java │ └── DraftActionPanel.java │ ├── exception │ └── JySrtToolsException.java │ ├── JyDraftLastModifiedTimeComparator.java │ ├── JyDraftNameComparator.java │ ├── JyFont.java │ ├── DraftTemplates.java │ ├── Subtitle.java │ ├── JySrtTools.java │ ├── JyDraft.java │ └── JyUtils.java ├── jycntw.icns ├── .gitignore ├── README.md ├── pom.xml └── LICENSE /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/icon.png -------------------------------------------------------------------------------- /src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | version=2.4.510 2 | jy_version=5.1.0 3 | -------------------------------------------------------------------------------- /jycntw.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/jycntw.icns -------------------------------------------------------------------------------- /src/main/resources/images/a-z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/a-z.png -------------------------------------------------------------------------------- /src/main/resources/images/z-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/z-a.png -------------------------------------------------------------------------------- /src/main/resources/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/close.png -------------------------------------------------------------------------------- /src/main/resources/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/icon.png -------------------------------------------------------------------------------- /src/main/resources/images/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/remove.png -------------------------------------------------------------------------------- /src/main/resources/images/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/reset.png -------------------------------------------------------------------------------- /src/main/resources/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/search.png -------------------------------------------------------------------------------- /src/main/resources/images/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/question.png -------------------------------------------------------------------------------- /src/main/resources/images/replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/replace.png -------------------------------------------------------------------------------- /src/main/resources/images/clockwise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/clockwise.png -------------------------------------------------------------------------------- /src/main/resources/draft_templates/draft_track.json: -------------------------------------------------------------------------------- 1 | { 2 | "flag" : 1, 3 | "id" : "${id}", 4 | "segments" : [], 5 | "type" : "text" 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/images/default_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/default_cover.png -------------------------------------------------------------------------------- /src/main/resources/images/counterclockwise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackychu0830/jy-srt-tools/HEAD/src/main/resources/images/counterclockwise.png -------------------------------------------------------------------------------- /src/main/resources/draft_templates/draft_extra_material.json: -------------------------------------------------------------------------------- 1 | { 2 | "animations" : [], 3 | "id" : "${id}", 4 | "type" : "sticker_animation" 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/DraftListModel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JyDraft; 4 | 5 | import javax.swing.*; 6 | 7 | public class DraftListModel extends DefaultListModel { 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | /jy-srt-tools.iml 25 | /.idea/ 26 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/exception/JySrtToolsException.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.exception; 2 | 3 | public class JySrtToolsException extends Throwable { 4 | public JySrtToolsException() { 5 | } 6 | 7 | public JySrtToolsException(String message) { 8 | super(message); 9 | } 10 | 11 | public JySrtToolsException(String message, Throwable th) { 12 | super(message, th); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/JyDraftLastModifiedTimeComparator.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import java.util.Comparator; 4 | 5 | public class JyDraftLastModifiedTimeComparator implements Comparator { 6 | 7 | @Override 8 | public int compare(JyDraft o1, JyDraft o2) { 9 | return Long.compare(o2.getLastModifiedTime(), o1.getLastModifiedTime()); 10 | } 11 | 12 | @Override 13 | public Comparator reversed() { 14 | return Comparator.super.reversed(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 剪映字幕工具箱 2 | 3 | ## 主要功能: 4 | 5 | 1. 草稿字幕簡體轉繁體 6 | 2. 字幕匯出成 SRT 檔 及 TXT 文字檔 7 | 3. 載入既有的 SRT 檔 8 | 4. 字幕編輯修改 9 | 5. 刪除影片字幕 10 | 11 | ## [Mac 版下載](https://github.com/jackychu0830/jy-srt-tools/tree/mac) 12 | 13 | ## [Windows 版下載](https://github.com/jackychu0830/jy-srt-tools/tree/win) 14 | 15 | ## 執行畫面 16 | 17 | ### Mac 18 | 19 | ![Mac 版畫面](https://github.com/jackychu0830/jy-srt-tools/raw/mac/screenshot-1.png) 20 | 21 | ### Windows 22 | 23 | ![Windows 版畫面](https://github.com/jackychu0830/jy-srt-tools/raw/win/screenshot-1.png) 24 | 25 | ## 使用說明 26 | 27 | https://youtu.be/ysV39jfLmfE 28 | -------------------------------------------------------------------------------- /src/main/resources/draft_templates/draft_subtitle_content.json: -------------------------------------------------------------------------------- 1 | { 2 | "styles": [ 3 | { 4 | "fill": { 5 | "alpha": 1.0, 6 | "content": { 7 | "render_type": "solid", 8 | "solid": { 9 | "alpha": 1.0, 10 | "color": [ 11 | 1.0, 12 | 1.0, 13 | 1.0 14 | ] 15 | } 16 | } 17 | }, 18 | "font": { 19 | "id": "", 20 | "path": "${font_path}" 21 | }, 22 | "range": [ 23 | 0, 24 | 10 25 | ], 26 | "size": 5.0 27 | } 28 | ], 29 | "text": "${text}" 30 | } -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/JyDraftNameComparator.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import java.util.Comparator; 4 | 5 | public class JyDraftNameComparator implements Comparator { 6 | 7 | @Override 8 | public int compare(JyDraft o1, JyDraft o2) { 9 | int res = String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()); 10 | if (res == 0) { 11 | res = o1.getName().compareTo(o2.getName()); 12 | } 13 | return res; 14 | } 15 | 16 | @Override 17 | public Comparator reversed() { 18 | return Comparator.super.reversed(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/AboutPanel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JyUtils; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | public class AboutPanel extends JPanel { 9 | public AboutPanel() { 10 | setLayout(new GridLayout(3, 1)); 11 | add(new JLabel("作業系統: " + System.getProperty("os.name") + "")); 12 | add(new JLabel("工具箱版本: " + JyUtils.getVersion() + "")); 13 | add(new JLabel("剪映版本: " + JyUtils.getJyVersion() + "")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/draft_templates/draft_segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "cartoon" : false, 3 | "clip" : { 4 | "alpha" : 1, 5 | "flip" : { 6 | "horizontal" : false, 7 | "vertical" : false 8 | }, 9 | "rotation" : 0, 10 | "scale" : { 11 | "x" : 1, 12 | "y" : 1 13 | }, 14 | "transform" : { 15 | "x" : 0, 16 | "y" : -0.73 17 | } 18 | }, 19 | "enable_adjust" : true, 20 | "enable_lut" : true, 21 | "extra_material_refs" : [ 22 | "${extra_material_refs}" 23 | ], 24 | "id" : "${id}", 25 | "intensifies_audio" : false, 26 | "is_tone_modify" : false, 27 | "keyframe_refs" : [], 28 | "last_nonzero_volume" : 1, 29 | "material_id" : "${material_id}", 30 | "render_index" : 14000, 31 | "reverse" : false, 32 | "source_timerange" : null, 33 | "speed" : 1, 34 | "target_timerange" : { 35 | "duration" : ${duration}, 36 | "start" : ${start} 37 | }, 38 | "volume" : 1 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/JyTextPanel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JySrtTools; 4 | import lombok.Getter; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | 9 | public class JyTextPanel extends JPanel { 10 | 11 | @Getter 12 | private final DraftListPanel listPanel; 13 | @Getter 14 | private final DraftTextsPanel textsPanel; 15 | @Getter 16 | private final DraftActionPanel actionPanel; 17 | 18 | public JyTextPanel(JySrtTools jySrtTools) { 19 | setLayout(new BorderLayout()); 20 | 21 | listPanel = new DraftListPanel(jySrtTools, this); 22 | add(listPanel, BorderLayout.WEST); 23 | 24 | textsPanel = new DraftTextsPanel(jySrtTools); 25 | add(textsPanel, BorderLayout.CENTER); 26 | 27 | actionPanel = new DraftActionPanel(jySrtTools, this); 28 | // actionPanel.setPreferredSize(new Dimension(230, 600)); 29 | add(actionPanel, BorderLayout.EAST); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/draft_templates/draft_text.json: -------------------------------------------------------------------------------- 1 | { 2 | "alignment": 1, 3 | "background_alpha": 1, 4 | "background_color": "", 5 | "bold_width": 0, 6 | "border_color": "", 7 | "border_width": 0.08, 8 | "content": "${content}", 9 | "font_id": "", 10 | "font_name": "", 11 | "font_path": "${font_path}", 12 | "font_resource_id": "", 13 | "font_size": 5, 14 | "font_title": "系统", 15 | "font_url": "", 16 | "has_shadow": false, 17 | "id": "${id}", 18 | "initial_scale": 1, 19 | "italic_degree": 0, 20 | "ktv_color": "", 21 | "layer_weight": 0, 22 | "letter_spacing": 0, 23 | "line_spacing": 0.02, 24 | "shadow_alpha": 0.8, 25 | "shadow_angle": -45, 26 | "shadow_color": "#000000", 27 | "shadow_distance": 8, 28 | "shadow_point": { 29 | "x": 1.0182337649086284, 30 | "y": -1.0182337649086284 31 | }, 32 | "shadow_smoothing": 1, 33 | "shape_clip_x": false, 34 | "shape_clip_y": false, 35 | "style_name": "", 36 | "sub_type": 0, 37 | "text_alpha": 1, 38 | "text_color": "#FFFFFF", 39 | "text_to_audio_ids": [], 40 | "type": "subtitle", 41 | "typesetting": 0, 42 | "underline": false, 43 | "underline_offset": 0.22, 44 | "underline_width": 0.05, 45 | "use_effect_default_color": true 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/DraftListCellRender.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JyDraft; 4 | import org.apache.commons.text.WordUtils; 5 | 6 | import javax.imageio.ImageIO; 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.io.IOException; 10 | import java.util.Objects; 11 | 12 | public class DraftListCellRender extends DefaultListCellRenderer { 13 | public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 14 | setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 20)); 15 | 16 | JyDraft draft = (JyDraft) value; 17 | setText("" + WordUtils.wrap(draft.getName(), 13, "
", true)); 18 | 19 | String icon = draft.getCoverFilename(); 20 | if (icon == null || icon.length() < 1) { 21 | try { 22 | Image image = ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/default_cover.png"))); 23 | setIcon(new ImageIcon(image)); 24 | } catch (IOException e) { 25 | e.printStackTrace(); 26 | } 27 | } else { 28 | setIcon(new ImageIcon(new ImageIcon(icon).getImage().getScaledInstance(320, 180, Image.SCALE_DEFAULT))); 29 | } 30 | 31 | if (isSelected) { 32 | setBackground(list.getSelectionBackground()); 33 | setForeground(list.getSelectionForeground()); 34 | } else { 35 | setBackground(list.getBackground()); 36 | setForeground(list.getForeground()); 37 | } 38 | return this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/SubtitleCellEditor.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.Subtitle; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.KeyEvent; 9 | import java.awt.event.MouseEvent; 10 | import java.util.EventObject; 11 | import java.util.Objects; 12 | 13 | public class SubtitleCellEditor extends DefaultCellEditor { 14 | JTextField textField; 15 | 16 | public SubtitleCellEditor() { 17 | super(new JTextField()); 18 | this.textField = (JTextField) this.getComponent(); 19 | } 20 | 21 | @Override 22 | public boolean isCellEditable(EventObject e) { 23 | if (e instanceof MouseEvent) { 24 | MouseEvent me = (MouseEvent) e; 25 | return me.getClickCount() >= 2; 26 | } else if (e instanceof KeyEvent) { 27 | KeyEvent ke = (KeyEvent) e; 28 | return ke.getKeyCode() == KeyEvent.VK_ENTER; 29 | } else if (e instanceof ActionEvent) { 30 | return Objects.equals(((ActionEvent) e).getActionCommand(), "\n"); 31 | } 32 | return false; 33 | } 34 | 35 | @Override 36 | public Object getCellEditorValue() { 37 | 38 | return this.textField.getText(); 39 | } 40 | 41 | @Override 42 | public Component getTableCellEditorComponent(JTable table, 43 | Object value, boolean isSelected, int row, int column) { 44 | this.textField.setText(((Subtitle) value).getText()); 45 | return this.textField; 46 | } 47 | 48 | public void focus() { 49 | this.textField.grabFocus(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/ErrorMessagePanel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | 4 | import javax.swing.*; 5 | import java.awt.*; 6 | 7 | public class ErrorMessagePanel extends JPanel { 8 | 9 | public ErrorMessagePanel(Throwable th) { 10 | super(); 11 | String message = th.getMessage() == null ? th.toString() : th.getMessage(); 12 | setLayout(new BorderLayout()); 13 | String title = message.split("(?<=!)")[0]; 14 | message += System.lineSeparator() + th.getClass(); 15 | message += System.lineSeparator() + System.lineSeparator() + trimStackTrace(th.getStackTrace()); 16 | JTextArea textArea = new JTextArea(message); 17 | textArea.setLineWrap(true); 18 | textArea.setEditable(false); 19 | JScrollPane jsp = new JScrollPane(textArea); 20 | jsp.setPreferredSize(new Dimension(600, 200)); 21 | 22 | JButton btnCopy = new JButton("複製錯誤訊息到剪貼簿"); 23 | btnCopy.addActionListener(e -> { 24 | textArea.selectAll(); 25 | textArea.copy(); 26 | }); 27 | 28 | add(new JLabel("" + title + ""), BorderLayout.NORTH); 29 | add(jsp, BorderLayout.CENTER); 30 | add(btnCopy, BorderLayout.SOUTH); 31 | } 32 | 33 | private String trimStackTrace(StackTraceElement[] stackTrace) { 34 | StringBuilder sb = new StringBuilder(); 35 | 36 | for (StackTraceElement element : stackTrace) { 37 | if (element.getClassName().startsWith("app.jackychu")) 38 | sb.append(element).append(System.lineSeparator()); 39 | } 40 | 41 | return sb.toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/SubtitleCellRender.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.Subtitle; 4 | 5 | import javax.swing.*; 6 | import javax.swing.table.TableCellRenderer; 7 | import java.awt.*; 8 | import java.util.Objects; 9 | 10 | public class SubtitleCellRender extends JLabel implements TableCellRenderer { 11 | 12 | public SubtitleCellRender() { 13 | super.setOpaque(true); 14 | } 15 | 16 | @Override 17 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 18 | Subtitle sub = (Subtitle) value; 19 | 20 | setFont(getFont().deriveFont(16f)); 21 | 22 | switch (column) { 23 | case 0: 24 | setText(String.valueOf(sub.getNum())); 25 | break; 26 | case 1: 27 | setText(Subtitle.msToTimeStr(sub.getStartTime())); 28 | break; 29 | case 2: 30 | setText(Subtitle.msToTimeStr(sub.getEndTime())); 31 | break; 32 | case 3: 33 | if (sub.getFindingText() != null && !Objects.equals(sub.getFindingText(), "")) { 34 | String str = sub.getText().replace(sub.getFindingText(), 35 | "" + sub.getFindingText() + ""); 36 | setText("" + str + ""); 37 | } else { 38 | setText(sub.getText()); 39 | } 40 | } 41 | 42 | if (isSelected) { 43 | setBackground(table.getSelectionBackground()); 44 | setForeground(table.getSelectionForeground()); 45 | } else { 46 | if (row % 2 == 0) { 47 | setBackground(table.getBackground()); 48 | } else { 49 | setBackground(new Color(220, 250, 250)); 50 | } 51 | setForeground(table.getForeground()); 52 | } 53 | 54 | return this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/DraftSubtitleTableModel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.Subtitle; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import javax.swing.table.DefaultTableModel; 8 | import java.util.List; 9 | import java.util.Objects; 10 | 11 | public class DraftSubtitleTableModel extends DefaultTableModel { 12 | @Getter 13 | private final List subtitles; 14 | private final String[] columnNames = new String[]{ 15 | "編號", "開始時間", "結束時間", "字幕" 16 | }; 17 | private final Class[] columnClass = new Class[]{ 18 | // Integer.class, String.class, String.class, String.class 19 | Subtitle.class, Subtitle.class, Subtitle.class, Subtitle.class 20 | }; 21 | @Getter 22 | @Setter 23 | private boolean dirty; 24 | 25 | public DraftSubtitleTableModel(List subtitles) { 26 | this.subtitles = subtitles; 27 | this.dirty = false; 28 | } 29 | 30 | @Override 31 | public String getColumnName(int column) { 32 | return columnNames[column]; 33 | } 34 | 35 | @Override 36 | public Class getColumnClass(int columnIndex) { 37 | return columnClass[columnIndex]; 38 | } 39 | 40 | @Override 41 | public int getRowCount() { 42 | if (subtitles == null) return 0; 43 | return subtitles.size(); 44 | } 45 | 46 | @Override 47 | public int getColumnCount() { 48 | return columnNames.length; 49 | } 50 | 51 | @Override 52 | public boolean isCellEditable(int rowIndex, int columnIndex) { 53 | return columnIndex == 3; 54 | } 55 | 56 | @Override 57 | public Object getValueAt(int rowIndex, int columnIndex) { 58 | return subtitles.get(rowIndex); 59 | } 60 | 61 | @Override 62 | public void setValueAt(Object value, int rowIndex, int columnIndex) { 63 | Subtitle sub = subtitles.get(rowIndex); 64 | if (columnIndex == 3) { 65 | if (!Objects.equals(sub.getText(), value.toString())) { 66 | this.dirty = true; 67 | sub.setText(value.toString()); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/JyFont.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import app.jackychu.jysrttools.exception.JySrtToolsException; 4 | import lombok.AccessLevel; 5 | import lombok.Data; 6 | import lombok.Setter; 7 | 8 | import java.awt.*; 9 | import java.io.File; 10 | import java.io.FileInputStream; 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | 15 | @Data 16 | public class JyFont { 17 | private String name; 18 | private String type; 19 | private File file; 20 | private Font font; 21 | private boolean replaced = false; 22 | private String replacedName; 23 | @Setter(AccessLevel.NONE) 24 | private Font backupFont; 25 | 26 | public JyFont(File file) throws JySrtToolsException { 27 | this.file = file; 28 | String[] s = file.getName().split("\\."); 29 | this.name = s[0]; 30 | this.type = s[s.length - 1]; 31 | 32 | try { 33 | GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment(); 34 | font = Font.createFont(Font.TRUETYPE_FONT, new FileInputStream(file)); 35 | genv.registerFont(font); 36 | font = font.deriveFont(20f); 37 | } catch (FontFormatException | IOException e) { 38 | throw new JySrtToolsException("讀取字型錯誤: " + name, e); 39 | } 40 | } 41 | 42 | public JyFont(File file, String replacedName) throws JySrtToolsException { 43 | this(file); 44 | replaced = true; 45 | this.replacedName = replacedName; 46 | setBackupFont(); 47 | } 48 | 49 | public void setBackupFont() throws JySrtToolsException { 50 | try { 51 | GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment(); 52 | backupFont = Font.createFont(Font.TRUETYPE_FONT, new FileInputStream(getBackupPath())); 53 | genv.registerFont(backupFont); 54 | backupFont = backupFont.deriveFont(20f); 55 | } catch (FontFormatException | IOException e) { 56 | throw new JySrtToolsException("讀取字型錯誤: " + getBackupPath(), e); 57 | } 58 | } 59 | 60 | public String getBackupPath() { 61 | if (replaced) { 62 | Path target = Paths.get(file.getAbsolutePath()); 63 | Path backupPath = Paths.get(target.getParent().toString(), name + "." + replacedName + ".bak"); 64 | return backupPath.toString(); 65 | } else { 66 | return null; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/DraftTemplates.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import app.jackychu.jysrttools.ui.ErrorMessagePanel; 4 | 5 | import javax.swing.*; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.Locale; 10 | 11 | public class DraftTemplates { 12 | public static final String MAC_DEFAULT_FONT_PATH = "/Applications/VideoFusion-macOS.app/Contents/Resources/Font/SystemFont/zh-hans.ttf"; 13 | public static final String WIN_DEFAULT_FONT_PATH = "${user_home}/AppData/Local/JianyingPro/Apps/${jy_version}/Resources/Font/SystemFont/zh-hans.ttf"; 14 | 15 | public static String getTemplate(String templateName) { 16 | String temp = null; 17 | try { 18 | InputStream in = DraftTemplates.class.getClassLoader().getResourceAsStream("draft_templates/" + templateName + ".json"); 19 | byte[] data = in.readAllBytes(); 20 | temp = new String(data); 21 | } catch (IOException | RuntimeException e) { 22 | JOptionPane.showMessageDialog(null, 23 | new ErrorMessagePanel(e), "讀取字幕模版失敗", JOptionPane.ERROR_MESSAGE); 24 | } 25 | 26 | return temp; 27 | } 28 | 29 | public static String getDefaultSubtitleFormat() { 30 | String os = System.getProperty("os.name"); 31 | String fontPath = null; 32 | if (os.toLowerCase(Locale.ROOT).contains("windows")) { 33 | fontPath = WIN_DEFAULT_FONT_PATH; 34 | } else { 35 | fontPath = MAC_DEFAULT_FONT_PATH; 36 | } 37 | 38 | String format = "${text}"; 39 | try { 40 | InputStream in = DraftTemplates.class.getClassLoader().getResourceAsStream("draft_templates/draft_subtitle_content.json"); 41 | // InputStream in = new FileInputStream("/Users/jacky.chu/workspace/jy-srt-tools/src/main/resources/draft_templates/draft_subtitle_content.json"); 42 | byte[] data = in.readAllBytes(); 43 | format = new String(data); 44 | format = format.replace("${font_path}", fontPath); 45 | } catch (IOException | RuntimeException e) { 46 | JOptionPane.showMessageDialog(null, 47 | new ErrorMessagePanel(e), "讀取預設字幕格式失敗", JOptionPane.ERROR_MESSAGE); 48 | } 49 | 50 | return format; 51 | } 52 | 53 | public static String getDefaultFontPath() { 54 | String os = System.getProperty("os.name"); 55 | if (os.toLowerCase(Locale.ROOT).contains("windows")) { 56 | return WIN_DEFAULT_FONT_PATH; 57 | } else { 58 | return MAC_DEFAULT_FONT_PATH; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/Subtitle.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.json.simple.JSONArray; 9 | import org.json.simple.JSONObject; 10 | import org.json.simple.parser.JSONParser; 11 | import org.json.simple.parser.ParseException; 12 | 13 | import java.util.Objects; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | @Data 18 | public class Subtitle implements Comparable { 19 | private String id; 20 | private int num; 21 | private long startTime; 22 | private long endTime; 23 | private long duration; 24 | private String findingText; 25 | private boolean found; 26 | private String text; 27 | private String contentFormat; 28 | 29 | /** 30 | * Convert time from ms to SRT time string format 31 | * 32 | * @param time time in ms 33 | * @return SRT time 34 | */ 35 | public static String msToTimeStr(long time) { 36 | long ms = time / 1000 % 1000; 37 | long sec = time / 1000 / 1000 % 60; 38 | long min = time / 1000 / 1000 / 60 % 60; 39 | long hour = time / 1000 / 1000 / 60 / 60; 40 | 41 | return String.format("%02d:%02d:%02d,%03d", hour, min, sec, ms); 42 | } 43 | 44 | /** 45 | * Convert time from STR time string format to ms 46 | * 47 | * @param str SRT time 48 | * @return time in ms 49 | */ 50 | public static long timeStrToMs(String str) { 51 | long time = 0; 52 | String[] t1 = str.split(","); 53 | String[] t2 = t1[0].split(":"); 54 | 55 | time += Long.parseLong(t2[0]) * 60 * 60 * 1000 * 1000; 56 | time += Long.parseLong(t2[1]) * 60 * 1000 * 1000; 57 | time += Long.parseLong(t2[2]) * 1000 * 1000; 58 | time += Long.parseLong(t1[1]) * 1000; 59 | 60 | return time; 61 | } 62 | 63 | public Subtitle() { 64 | 65 | } 66 | 67 | public Subtitle(String id) { 68 | this.id = id; 69 | } 70 | @Override 71 | public int compareTo(Subtitle sub) { 72 | return (int) (this.startTime - sub.startTime); 73 | } 74 | 75 | @Override 76 | public boolean equals(Object o) { 77 | if (this == o) return true; 78 | if (o == null || getClass() != o.getClass()) return false; 79 | Subtitle subtitle = (Subtitle) o; 80 | return subtitle.id.equals(this.getId()); 81 | } 82 | 83 | @Override 84 | public int hashCode() { 85 | return Objects.hash(id, num, text, startTime, endTime, duration); 86 | } 87 | 88 | public String getFormattedText() { 89 | try { 90 | JSONObject content = (JSONObject) (new JSONParser()).parse(this.contentFormat); 91 | content.put("text", this.text); 92 | JSONArray range = new JSONArray(); 93 | range.add(0); 94 | range.add(this.text.length()); 95 | JSONArray styles = (JSONArray) content.get("styles"); 96 | JSONObject style = (JSONObject) styles.get(0); 97 | style.put("range", range); 98 | styles.set(0, style); 99 | content.put("styles", styles); 100 | return content.toString(); 101 | } catch (ParseException e) { 102 | e.printStackTrace(); 103 | return this.contentFormat.replace("${text}", this.text); 104 | } 105 | 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/TranslateProgressDialog.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JyDraft; 4 | import app.jackychu.jysrttools.JyUtils; 5 | import app.jackychu.jysrttools.Subtitle; 6 | import com.github.houbb.opencc4j.util.ZhConverterUtil; 7 | import com.github.houbb.opencc4j.util.ZhTwConverterUtil; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | public class TranslateProgressDialog extends JDialog { 16 | private final JFrame parent; 17 | private final JProgressBar progressBar; 18 | private final JLabel label; 19 | private final JButton btnOk; 20 | 21 | public TranslateProgressDialog(JFrame parent, boolean modal) { 22 | super(parent, modal); 23 | this.parent = parent; 24 | setSize(300, 150); 25 | setLocationRelativeTo(null); 26 | setResizable(false); 27 | setLayout(new BorderLayout()); 28 | setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); 29 | 30 | label = new JLabel("翻譯中… 0%", JLabel.CENTER); 31 | label.setFont(label.getFont().deriveFont(20f)); 32 | add(label, BorderLayout.NORTH); 33 | 34 | progressBar = new JProgressBar(0, 100); 35 | progressBar.setBorder(BorderFactory.createEmptyBorder(5, 20, 5, 20)); 36 | progressBar.setValue(0); 37 | add(progressBar, BorderLayout.CENTER); 38 | 39 | btnOk = new JButton("完成"); 40 | btnOk.setEnabled(false); 41 | btnOk.addActionListener(e -> 42 | ((JButton) e.getSource()).getParent().getParent().getParent().getParent().setVisible(false)); 43 | add(btnOk, BorderLayout.SOUTH); 44 | } 45 | 46 | public void doTranslate(String type, JyDraft draft) { 47 | progressBar.setValue(0); 48 | label.setText("翻譯中… 0%"); 49 | btnOk.setEnabled(false); 50 | 51 | SwingWorker sw = new SwingWorker<>() { 52 | 53 | @Override 54 | protected Void doInBackground() { 55 | int i = 0; 56 | int percentage; 57 | 58 | try { 59 | java.util.List newSubs = new ArrayList<>(); 60 | 61 | int size = draft.getSubtitles().size(); 62 | for (Subtitle sub : draft.getSubtitles()) { 63 | percentage = (int) ((++i * 1.0) / size * 100); 64 | progressBar.setValue(percentage); 65 | label.setText(String.format("翻譯中… %d%%", percentage)); 66 | 67 | String newStr = sub.getText(); 68 | if (ZhConverterUtil.isTraditional(newStr)) continue; 69 | if (type.equals("tcTranslate")) { 70 | newStr = ZhConverterUtil.toTraditional(newStr); 71 | } else { // twTranslate 72 | newStr = ZhTwConverterUtil.toTraditional(newStr); 73 | } 74 | 75 | sub.setText(newStr); 76 | draft.updateDraftSubtitle(sub); 77 | } 78 | 79 | // Save translated texts back to Jy draft 80 | JyUtils.saveDraft(draft); 81 | 82 | label.setText("翻譯完畢"); 83 | progressBar.setValue(100); 84 | 85 | } catch (Throwable e) { 86 | setVisible(false); 87 | JOptionPane.showMessageDialog(parent, 88 | new ErrorMessagePanel(e), "翻譯失敗", JOptionPane.ERROR_MESSAGE); 89 | } finally { 90 | btnOk.setEnabled(true); 91 | } 92 | return null; 93 | } 94 | }; 95 | 96 | sw.execute(); 97 | setVisible(true); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/JySrtTools.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import app.jackychu.jysrttools.exception.JySrtToolsException; 4 | import app.jackychu.jysrttools.ui.*; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import javax.imageio.ImageIO; 9 | import javax.swing.*; 10 | import java.awt.*; 11 | import java.awt.event.ComponentAdapter; 12 | import java.awt.event.ComponentEvent; 13 | import java.io.IOException; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | public class JySrtTools extends JFrame { 19 | private List drafts; 20 | @Getter 21 | @Setter 22 | private JyDraft currentSelectedDraft = null; 23 | @Getter 24 | private TranslateProgressDialog progressDialog; 25 | @Getter 26 | private FindReplaceDialog findReplaceDialog; 27 | @Getter 28 | private JyTextPanel jyTextPanel; 29 | 30 | public JySrtTools() { 31 | try { 32 | loadDrafts(); 33 | init(); 34 | } catch (Throwable e) { 35 | JOptionPane.showMessageDialog(this, 36 | new ErrorMessagePanel(e), "程式錯誤", JOptionPane.ERROR_MESSAGE); 37 | System.exit(1); 38 | } 39 | } 40 | 41 | public static void main(String[] args) { 42 | try { 43 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 44 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) { 45 | e.printStackTrace(); 46 | } 47 | 48 | SwingUtilities.invokeLater(() -> { 49 | JySrtTools tools = new JySrtTools(); 50 | tools.setVisible(true); 51 | }); 52 | } 53 | 54 | private void init() { 55 | setLayout(new BorderLayout()); 56 | 57 | setTitle("剪映字幕工具箱"); 58 | setMinimumSize(new Dimension(1385, 335)); 59 | setLocationRelativeTo(null); 60 | setDefaultCloseOperation(EXIT_ON_CLOSE); 61 | 62 | setVisible(true); 63 | setExtendedState(getExtendedState() | JFrame.MAXIMIZED_BOTH); 64 | 65 | // 限制主視窗無法縮的比 minimum size 還要小 66 | addComponentListener(new ComponentAdapter() { 67 | public void componentResized(ComponentEvent e) { 68 | Dimension d = getSize(); 69 | Dimension minD = getMinimumSize(); 70 | if (d.width < minD.width) 71 | d.width = minD.width; 72 | if (d.height < minD.height) 73 | d.height = minD.height; 74 | setSize(d); 75 | } 76 | }); 77 | 78 | try { 79 | Image image = ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/icon.png"))); 80 | setIconImage(new ImageIcon(image).getImage()); 81 | } catch (IOException e) { 82 | e.printStackTrace(); 83 | } 84 | 85 | jyTextPanel = new JyTextPanel(this); 86 | add(jyTextPanel); 87 | setJMenuBar(new JyMenuBar(this)); 88 | 89 | progressDialog = new TranslateProgressDialog(this, true); 90 | findReplaceDialog = new FindReplaceDialog(this, false); 91 | } 92 | 93 | public void loadDrafts() throws JySrtToolsException { 94 | drafts = JyUtils.getAllJyDrafts(); 95 | } 96 | 97 | public List getDrafts() { 98 | return getDrafts(new JyDraftLastModifiedTimeComparator(), false); 99 | } 100 | 101 | public List getDrafts(Comparator comp, boolean reverse) { 102 | if (reverse) 103 | drafts.sort(comp.reversed()); 104 | else 105 | drafts.sort(comp); 106 | return drafts; 107 | } 108 | 109 | public List filterDrafts(String text) { 110 | if (text.trim().equals("")) { 111 | for (JyDraft draft : drafts) { 112 | draft.setHidden(false); 113 | } 114 | } else { 115 | for (JyDraft draft : drafts) { 116 | draft.setHidden(!draft.getName().contains(text)); 117 | } 118 | } 119 | 120 | return drafts; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/JyMenuBar.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JySrtTools; 4 | import app.jackychu.jysrttools.exception.JySrtToolsException; 5 | 6 | import javax.imageio.ImageIO; 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.awt.event.ActionListener; 10 | import java.awt.event.KeyEvent; 11 | import java.io.IOException; 12 | import java.util.Locale; 13 | import java.util.Objects; 14 | 15 | public class JyMenuBar extends JMenuBar { 16 | private final JySrtTools jySrtTools; 17 | private final JMenuItem aboutMenuItem; 18 | private final JMenuItem findMenuItem; 19 | private final JMenuItem replaceMenuItem; 20 | 21 | public JyMenuBar(JySrtTools jySrtTools) { 22 | super(); 23 | 24 | String os = System.getProperty("os.name"); 25 | int cmdKey = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); //KeyEvent.VK_META; 26 | if (os.toLowerCase(Locale.ROOT).contains("windows")) { 27 | cmdKey = KeyEvent.CTRL_DOWN_MASK; 28 | } 29 | 30 | this.jySrtTools = jySrtTools; 31 | JMenu fileMenu = new JMenu("檔案"); 32 | 33 | aboutMenuItem = new JMenuItem("關於"); 34 | aboutMenuItem.setToolTipText("關於本程式"); 35 | addAboutMenuItemActionListener(); 36 | fileMenu.add(aboutMenuItem); 37 | 38 | JMenuItem exiMenuItem = new JMenuItem("結束"); 39 | exiMenuItem.setToolTipText("結束程式"); 40 | exiMenuItem.addActionListener((event) -> System.exit(0)); 41 | fileMenu.add(exiMenuItem); 42 | 43 | add(fileMenu); 44 | 45 | JMenu editMenu = new JMenu("編輯"); 46 | findMenuItem = new JMenuItem("尋找"); 47 | findMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, cmdKey)); 48 | findMenuItem.setToolTipText("尋找字串"); 49 | editMenu.add(findMenuItem); 50 | 51 | replaceMenuItem = new JMenuItem("替換"); 52 | replaceMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, cmdKey)); 53 | replaceMenuItem.setToolTipText("尋找並替換字串"); 54 | editMenu.add(replaceMenuItem); 55 | 56 | addMenuItemActionListener(); 57 | 58 | add(editMenu); 59 | } 60 | 61 | private void addAboutMenuItemActionListener() { 62 | aboutMenuItem.addActionListener(e -> { 63 | Object[] options = {"知道了"}; 64 | ImageIcon icon = null; 65 | try { 66 | Image image = ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/icon.png"))); 67 | icon = new ImageIcon(new ImageIcon(image).getImage().getScaledInstance(96, 96, Image.SCALE_DEFAULT)); 68 | 69 | } catch (IOException ioe) { 70 | ioe.printStackTrace(); 71 | } 72 | 73 | JOptionPane.showOptionDialog(jySrtTools, 74 | new AboutPanel(), 75 | "關於本程式", 76 | JOptionPane.DEFAULT_OPTION, 77 | JOptionPane.INFORMATION_MESSAGE, 78 | icon, 79 | options, 80 | options[0]); 81 | }); 82 | } 83 | 84 | private void addMenuItemActionListener() { 85 | ActionListener actionListener = e -> { 86 | if (jySrtTools.getCurrentSelectedDraft() != null) { 87 | if (jySrtTools.getCurrentSelectedDraft().getSubtitles().isEmpty()) { 88 | JOptionPane.showMessageDialog(jySrtTools, 89 | "請選選擇有字幕的草稿檔", "找不到字幕", JOptionPane.INFORMATION_MESSAGE); 90 | } else { 91 | if (jySrtTools.getJyTextPanel().getTextsPanel().getSubtitleTable().isEditing()) { 92 | jySrtTools.getJyTextPanel().getTextsPanel().getSubtitleTable().getCellEditor().cancelCellEditing(); 93 | } 94 | jySrtTools.getFindReplaceDialog().setVisible(true); 95 | jySrtTools.getFindReplaceDialog().setReplaceMode(e.getSource() == replaceMenuItem); 96 | } 97 | } 98 | 99 | }; 100 | findMenuItem.addActionListener(actionListener); 101 | replaceMenuItem.addActionListener(actionListener); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/DraftTextsPanel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JyDraft; 4 | import app.jackychu.jysrttools.JySrtTools; 5 | import app.jackychu.jysrttools.JyUtils; 6 | import app.jackychu.jysrttools.Subtitle; 7 | import app.jackychu.jysrttools.exception.JySrtToolsException; 8 | import lombok.Getter; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | import java.awt.event.ActionEvent; 13 | import java.awt.event.KeyEvent; 14 | import java.util.ArrayList; 15 | 16 | public class DraftTextsPanel extends JPanel { 17 | private final JySrtTools jySrtTools; 18 | @Getter 19 | private final JTable subtitleTable; 20 | 21 | public DraftTextsPanel(JySrtTools jySrtTools) { 22 | this.jySrtTools = jySrtTools; 23 | setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 24 | 25 | JLabel label = new JLabel("步驟二: 檢視草稿字幕內容", JLabel.LEFT); 26 | 27 | subtitleTable = new JTable(new DraftSubtitleTableModel(new ArrayList<>())); 28 | subtitleTable.setDefaultRenderer(Subtitle.class, new SubtitleCellRender()); 29 | subtitleTable.setDefaultEditor(Subtitle.class, new SubtitleCellEditor()); 30 | subtitleTable.setRowHeight(24); 31 | subtitleTable.getTableHeader().setReorderingAllowed(false); 32 | subtitleTable.getTableHeader().setFont(subtitleTable.getTableHeader().getFont().deriveFont(16f)); 33 | subtitleTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 34 | setTableColumnSize(); 35 | 36 | InputMap im = subtitleTable.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 37 | KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); 38 | im.put(enter, "startEditing"); // Change action from "selectNextRow" to "startEditing" 39 | // im.getParent().put(enter, "startEditing"); // Can enter edit mode. However, only focus on 字幕 cell 40 | subtitleTable.getActionMap().put(im.get(enter), new AbstractAction() { // Redefine "startEditing" action 41 | public void actionPerformed(ActionEvent e) { 42 | int row = subtitleTable.getSelectedRow(); 43 | subtitleTable.editCellAt(row, 3, e); 44 | // subtitleTable.editCellAt(row, 3, new KeyEvent((Component) e.getSource(), 401, e.getWhen(), 45 | // e.getModifiers(), 10, '\n' )); 46 | ((SubtitleCellEditor)subtitleTable.getCellEditor(row, 3)).focus(); 47 | } 48 | }); 49 | 50 | 51 | subtitleTable.addPropertyChangeListener("tableCellEditor", e -> { 52 | if (!subtitleTable.isEditing()) { 53 | DraftSubtitleTableModel dm = (DraftSubtitleTableModel) subtitleTable.getModel(); 54 | if (dm.isDirty()) { 55 | Subtitle sub = dm.getSubtitles().get(subtitleTable.getSelectedRow()); 56 | saveSubtitleChanges(sub); 57 | dm.setDirty(false); 58 | } 59 | } 60 | }); 61 | 62 | setLayout(new BorderLayout()); 63 | add(label, BorderLayout.NORTH); 64 | JScrollPane jsp = new JScrollPane(subtitleTable); 65 | jsp.setPreferredSize(new Dimension(500, 600)); 66 | 67 | add(new JScrollPane(subtitleTable), BorderLayout.CENTER); 68 | } 69 | 70 | public void saveSubtitleChanges(Subtitle sub) { 71 | try { 72 | this.jySrtTools.getCurrentSelectedDraft().updateDraftSubtitle(sub); 73 | JyUtils.saveDraft(this.jySrtTools.getCurrentSelectedDraft()); 74 | } catch (JySrtToolsException ex) { 75 | JOptionPane.showMessageDialog(this, 76 | new ErrorMessagePanel(ex), "儲存修改失敗", JOptionPane.ERROR_MESSAGE); 77 | } 78 | } 79 | 80 | public void setSubtitles(JyDraft draft) throws JySrtToolsException { 81 | subtitleTable.setModel(new DraftSubtitleTableModel(new ArrayList<>())); 82 | setTableColumnSize(); 83 | 84 | if (draft == null) return; 85 | if (draft.getSubtitles().isEmpty()) { 86 | jySrtTools.getJyTextPanel().getActionPanel().enableButtons(false); 87 | } else { 88 | subtitleTable.setModel(new DraftSubtitleTableModel(draft.getSubtitles())); 89 | subtitleTable.grabFocus(); 90 | subtitleTable.setRowSelectionInterval(0,0); 91 | jySrtTools.getJyTextPanel().getActionPanel().enableButtons(true); 92 | } 93 | setTableColumnSize(); 94 | } 95 | 96 | private void setTableColumnSize() { 97 | // subtitleTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 98 | subtitleTable.getColumnModel().getColumn(0).setMaxWidth(50); 99 | subtitleTable.getColumnModel().getColumn(0).setResizable(false); 100 | subtitleTable.getColumnModel().getColumn(1).setMaxWidth(115); 101 | subtitleTable.getColumnModel().getColumn(1).setMinWidth(115); 102 | subtitleTable.getColumnModel().getColumn(1).setResizable(false); 103 | subtitleTable.getColumnModel().getColumn(2).setMaxWidth(115); 104 | subtitleTable.getColumnModel().getColumn(2).setMinWidth(115); 105 | subtitleTable.getColumnModel().getColumn(2).setResizable(false); 106 | // subtitleTable.getColumnModel().getColumn(3).setMinWidth(270); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/SearchBox.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import javax.imageio.ImageIO; 4 | import javax.swing.*; 5 | import javax.swing.border.Border; 6 | import java.awt.*; 7 | import java.awt.event.*; 8 | import java.awt.geom.RoundRectangle2D; 9 | import java.io.IOException; 10 | import java.util.Objects; 11 | 12 | public class SearchBox extends JTextField implements KeyListener, MouseListener, MouseMotionListener { 13 | private static final int ICON_SPACING = 4; 14 | private Shape shape; 15 | private Border mBorder; 16 | private ImageIcon searchIcon; 17 | private ImageIcon cleanIcon; 18 | private boolean showCleanIcon = false; 19 | 20 | public SearchBox(int size) { 21 | super(size); 22 | try { 23 | searchIcon = resizeIcon(new ImageIcon(ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/search.png"))))); 24 | cleanIcon = resizeIcon(new ImageIcon(ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/close.png"))))); 25 | 26 | cleanIcon.setDescription("hello"); 27 | } catch (IOException e) { 28 | e.printStackTrace(); 29 | } 30 | addKeyListener(this); 31 | addMouseListener(this); 32 | addMouseMotionListener(this); 33 | setOpaque(false); 34 | resetBorder(); 35 | } 36 | 37 | @Override 38 | protected void paintComponent(Graphics g) { 39 | g.setColor(getBackground()); 40 | g.fillRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 15, 15); 41 | if (searchIcon != null) { 42 | Insets iconInsets = mBorder.getBorderInsets(this); 43 | searchIcon.paintIcon(this, g, iconInsets.left, iconInsets.top); 44 | } 45 | if (cleanIcon != null && showCleanIcon) { 46 | Insets iconInsets = mBorder.getBorderInsets(this); 47 | cleanIcon.paintIcon(this, g, this.getWidth() - cleanIcon.getIconWidth() - ICON_SPACING, iconInsets.top); 48 | } 49 | super.paintComponent(g); 50 | } 51 | 52 | @Override 53 | protected void paintBorder(Graphics g) { 54 | g.setColor(getForeground()); 55 | g.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 15, 15); 56 | } 57 | 58 | @Override 59 | public boolean contains(int x, int y) { 60 | if (shape == null || !shape.getBounds().equals(getBounds())) { 61 | shape = new RoundRectangle2D.Float(0, 0, getWidth() - 1, getHeight() - 1, 15, 15); 62 | } 63 | return shape.contains(x, y); 64 | } 65 | 66 | @Override 67 | public void setBorder(Border border) { 68 | mBorder = border; 69 | 70 | if (searchIcon == null && cleanIcon == null) { 71 | super.setBorder(border); 72 | } else { 73 | Border margin; 74 | if (cleanIcon == null) { // searchIcon 75 | margin = BorderFactory.createEmptyBorder(0, searchIcon.getIconWidth() + ICON_SPACING, 0, 0); 76 | } else { 77 | if (showCleanIcon && !getText().equals("")) { 78 | if (searchIcon == null) { 79 | margin = BorderFactory.createEmptyBorder(0, 0, 0, cleanIcon.getIconWidth() + ICON_SPACING); 80 | } else { 81 | margin = BorderFactory.createEmptyBorder(0, searchIcon.getIconWidth() + ICON_SPACING, 0, cleanIcon.getIconWidth() + ICON_SPACING); 82 | } 83 | } else { 84 | if (searchIcon == null) { 85 | margin = BorderFactory.createEmptyBorder(0, 0, 0, 0); 86 | } else { 87 | margin = BorderFactory.createEmptyBorder(0, searchIcon.getIconWidth() + ICON_SPACING, 0, 0); 88 | } 89 | } 90 | } 91 | Border compound = BorderFactory.createCompoundBorder(border, margin); 92 | super.setBorder(compound); 93 | } 94 | } 95 | 96 | private ImageIcon resizeIcon(ImageIcon icon) { 97 | if (icon.getIconWidth() > 16) { 98 | return new ImageIcon(icon.getImage().getScaledInstance(16, 16, Image.SCALE_DEFAULT)); 99 | } else { 100 | return icon; 101 | } 102 | } 103 | 104 | private void resetBorder() { 105 | setBorder(mBorder); 106 | } 107 | 108 | @Override 109 | public void keyTyped(KeyEvent e) { 110 | showCleanIcon = !getText().equals(""); 111 | } 112 | 113 | @Override 114 | public void keyPressed(KeyEvent e) { 115 | } 116 | 117 | @Override 118 | public void keyReleased(KeyEvent e) { 119 | if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { 120 | setText(""); 121 | showCleanIcon = false; 122 | } else { 123 | showCleanIcon = !getText().equals(""); 124 | } 125 | } 126 | 127 | @Override 128 | public void mouseClicked(MouseEvent e) { 129 | if (isOnCleanIcon(e.getLocationOnScreen())) { 130 | setText(""); 131 | showCleanIcon = false; 132 | } 133 | } 134 | 135 | @Override 136 | public void mousePressed(MouseEvent e) { 137 | 138 | } 139 | 140 | @Override 141 | public void mouseReleased(MouseEvent e) { 142 | 143 | } 144 | 145 | @Override 146 | public void mouseEntered(MouseEvent e) { 147 | } 148 | 149 | @Override 150 | public void mouseExited(MouseEvent e) { 151 | 152 | } 153 | 154 | @Override 155 | public void mouseDragged(MouseEvent e) { 156 | 157 | } 158 | 159 | @Override 160 | public void mouseMoved(MouseEvent e) { 161 | if (isOnCleanIcon(e.getLocationOnScreen())) { 162 | setCursor(new Cursor(Cursor.HAND_CURSOR)); 163 | setToolTipText("清除搜尋 (也可按下 ESC 鍵)"); 164 | } else { 165 | setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); 166 | } 167 | } 168 | 169 | private boolean isOnCleanIcon(Point p) { 170 | if (showCleanIcon) { 171 | Point sp = getLocationOnScreen(); 172 | return p.x >= sp.x + getWidth() - cleanIcon.getIconWidth() - ICON_SPACING && 173 | p.x <= sp.x + getWidth() && 174 | p.y <= sp.y + getHeight() && p.y >= sp.y; 175 | } else { 176 | return false; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | app.jackychu 5 | jy-srt-tools 6 | jar 7 | 2.0.200 8 | 9 | jy-srt-tools 10 | The SRT tools for JianyingPro video editor 11 | https://github.com/jackychu0830/jy-srt-tools 12 | 13 | 14 | 15 | 16 | org.apache.maven.plugins 17 | maven-compiler-plugin 18 | 3.8.0 19 | 20 | 11 21 | 11 22 | 23 | 24 | 25 | org.apache.maven.plugins 26 | maven-jar-plugin 27 | 3.0.2 28 | 29 | 30 | 31 | true 32 | app.jackychu.jysrttools.JySrtTools 33 | 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-resources-plugin 40 | 3.2.0 41 | 42 | 43 | copy-resources 44 | package 45 | 46 | copy-resources 47 | 48 | 49 | ${basedir}/target/extra-resources/classes 50 | 51 | 52 | src/resources 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.sonatype.oss 65 | oss-parent 66 | 9 67 | 68 | 69 | 70 | 71 | Apache License, Version 2.0 72 | https://www.apache.org/licenses/LICENSE-2.0 73 | 74 | 75 | 76 | 77 | 78 | Jacky Chu 79 | jacky.ju@gmail.com 80 | 81 | 82 | 83 | 84 | main 85 | https://github.com/jackychu0830/jy-srt-tools.git 86 | scm:git:https://github.com/jackychu0830/jy-srt-tools.git 87 | scm:git:https://github.com/jackychu0830/jy-srt-tools.git 88 | 89 | 90 | 91 | 92 | UTF-8 93 | 1.8 94 | 1.8 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | junit 107 | junit 108 | 4.13.2 109 | test 110 | 111 | 112 | com.googlecode.json-simple 113 | json-simple 114 | 1.1.1 115 | 116 | 117 | commons-codec 118 | commons-codec 119 | 1.15 120 | 121 | 122 | org.projectlombok 123 | lombok 124 | 1.18.22 125 | provided 126 | 127 | 128 | org.apache.commons 129 | commons-text 130 | 1.9 131 | 132 | 133 | com.github.houbb 134 | opencc4j 135 | 1.7.2 136 | 137 | 138 | org.apache.commons 139 | commons-lang3 140 | 3.12.0 141 | 142 | 143 | 144 | 145 | 146 | standalone 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-assembly-plugin 152 | 153 | 154 | package 155 | 156 | single 157 | 158 | 159 | 160 | 161 | 162 | jar-with-dependencies 163 | 164 | 165 | 166 | true 167 | app.jackychu.jysrttools.JySrtTools 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/DraftListPanel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JyDraft; 4 | import app.jackychu.jysrttools.JyDraftLastModifiedTimeComparator; 5 | import app.jackychu.jysrttools.JyDraftNameComparator; 6 | import app.jackychu.jysrttools.JySrtTools; 7 | import app.jackychu.jysrttools.exception.JySrtToolsException; 8 | 9 | import javax.imageio.ImageIO; 10 | import javax.swing.*; 11 | import javax.swing.event.DocumentEvent; 12 | import javax.swing.event.DocumentListener; 13 | import java.awt.*; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Objects; 18 | 19 | public class DraftListPanel extends JPanel { 20 | private final JySrtTools jySrtTools; 21 | private final JyTextPanel parent; 22 | private final JList list; 23 | private final SearchBox searchBox; 24 | private final JButton btnAzSort; 25 | private final JButton btnTimeSort; 26 | private ImageIcon azIcon; 27 | private ImageIcon zaIcon; 28 | private ImageIcon clockwiseIcon; 29 | private ImageIcon counterclockwiseIcon; 30 | 31 | public DraftListPanel(JySrtTools jySrtTools, JyTextPanel parent) { 32 | try { 33 | azIcon = new ImageIcon(ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/a-z.png")))); 34 | zaIcon = new ImageIcon(ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/z-a.png")))); 35 | clockwiseIcon = new ImageIcon(ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/clockwise.png")))); 36 | counterclockwiseIcon = new ImageIcon(ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/counterclockwise.png")))); 37 | } catch (IOException e) { 38 | e.printStackTrace(); 39 | } 40 | 41 | this.jySrtTools = jySrtTools; 42 | this.parent = parent; 43 | setBorder(BorderFactory.createEmptyBorder(5, 20, 5, 5)); 44 | 45 | JPanel topPanel = new JPanel(); 46 | topPanel.setLayout(new GridLayout(2, 1)); 47 | 48 | JPanel titlePanel = new JPanel(); 49 | titlePanel.setLayout(new BorderLayout()); 50 | JLabel label = new JLabel("步驟一: 選擇影片草稿", JLabel.LEFT); 51 | titlePanel.add(label, BorderLayout.WEST); 52 | JButton btnReload = new JButton("重新載入"); 53 | setButtonActionListener(btnReload); 54 | titlePanel.add(btnReload, BorderLayout.EAST); 55 | topPanel.add(titlePanel); 56 | 57 | JPanel searchPanel = new JPanel(); 58 | searchPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); 59 | searchBox = new SearchBox(32); 60 | addSearchBoxListener(); 61 | searchPanel.add(searchBox); 62 | btnAzSort = new JButton(); 63 | btnAzSort.setIcon(azIcon); 64 | btnAzSort.setToolTipText("照草稿名稱排序"); 65 | addSortButtonListener(btnAzSort); 66 | btnTimeSort = new JButton(); 67 | btnTimeSort.setIcon(clockwiseIcon); 68 | btnTimeSort.setToolTipText("照草稿最後編輯時間排序"); 69 | addSortButtonListener(btnTimeSort); 70 | searchPanel.add(btnAzSort); 71 | searchPanel.add(btnTimeSort); 72 | topPanel.add(searchPanel); 73 | 74 | list = new JList<>(getListModel(jySrtTools.getDrafts())); 75 | list.setCellRenderer(new DraftListCellRender()); 76 | list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 77 | setListEventListener(); 78 | JScrollPane jsp = new JScrollPane(list); 79 | jsp.setHorizontalScrollBar(null); 80 | 81 | setLayout(new BorderLayout()); 82 | add(topPanel, BorderLayout.NORTH); 83 | add(jsp, BorderLayout.CENTER); 84 | } 85 | 86 | private void setButtonActionListener(JButton btn) { 87 | btn.addActionListener(event -> { 88 | try { 89 | jySrtTools.loadDrafts(); 90 | jySrtTools.getJyTextPanel().getListPanel().reloadList(jySrtTools.getDrafts()); 91 | jySrtTools.getJyTextPanel().getTextsPanel().setSubtitles(null); 92 | } catch (JySrtToolsException jye) { 93 | jySrtTools.getJyTextPanel().getActionPanel().enableButtons(false, true); 94 | JOptionPane.showMessageDialog(jySrtTools, 95 | new ErrorMessagePanel(jye), "重新載入草稿錯誤", JOptionPane.ERROR_MESSAGE); 96 | } 97 | jySrtTools.getJyTextPanel().getActionPanel().enableButtons(false, true); 98 | }); 99 | } 100 | 101 | private void setListEventListener() { 102 | list.addListSelectionListener(event -> { 103 | if (!event.getValueIsAdjusting()) { 104 | JList list = (JList) event.getSource(); 105 | jySrtTools.setCurrentSelectedDraft(list.getSelectedValue()); 106 | parent.getActionPanel().enableButtons(false); 107 | try { 108 | parent.getTextsPanel().setSubtitles(jySrtTools.getCurrentSelectedDraft()); 109 | } catch (JySrtToolsException e) { 110 | JOptionPane.showMessageDialog(jySrtTools, 111 | new ErrorMessagePanel(e), "草稿資料讀取錯誤", JOptionPane.ERROR_MESSAGE); 112 | } 113 | } 114 | 115 | }); 116 | } 117 | 118 | public void reloadList(List drafts) { 119 | list.removeAll(); 120 | list.setModel(getListModel(drafts)); 121 | } 122 | 123 | private DraftListModel getListModel(List drafts) { 124 | List newDraftList = new ArrayList<>(); 125 | for (JyDraft draft : drafts) { 126 | if (!draft.isHidden()) { 127 | newDraftList.add(draft); 128 | } 129 | } 130 | DraftListModel model = new DraftListModel(); 131 | model.addAll(newDraftList); 132 | return model; 133 | } 134 | 135 | private void addSearchBoxListener() { 136 | searchBox.getDocument().addDocumentListener(new DocumentListener() { 137 | @Override 138 | public void insertUpdate(DocumentEvent e) { 139 | reloadList(jySrtTools.filterDrafts(searchBox.getText())); 140 | } 141 | 142 | @Override 143 | public void removeUpdate(DocumentEvent e) { 144 | reloadList(jySrtTools.filterDrafts(searchBox.getText())); 145 | } 146 | 147 | @Override 148 | public void changedUpdate(DocumentEvent e) { 149 | } 150 | }); 151 | } 152 | 153 | private void addSortButtonListener(JButton btn) { 154 | if (btn == btnAzSort) { 155 | btnAzSort.addActionListener(e -> { 156 | Icon icon = btnAzSort.getIcon(); 157 | List drafts = null; 158 | if (icon == azIcon) { 159 | drafts = jySrtTools.getDrafts((new JyDraftNameComparator()).reversed(), true); 160 | btnAzSort.setIcon(zaIcon); 161 | btnAzSort.setToolTipText("照草稿名稱排序 (反向)"); 162 | } else if (icon == zaIcon) { 163 | drafts = jySrtTools.getDrafts((new JyDraftNameComparator()).reversed(), false); 164 | btnAzSort.setIcon(azIcon); 165 | btnAzSort.setToolTipText("照草稿名稱排序"); 166 | } 167 | reloadList(drafts); 168 | }); 169 | } else if (btn == btnTimeSort) { 170 | btnTimeSort.addActionListener(e -> { 171 | Icon icon = btnTimeSort.getIcon(); 172 | List drafts; 173 | if (icon == clockwiseIcon) { 174 | drafts = jySrtTools.getDrafts(new JyDraftLastModifiedTimeComparator(), true); 175 | btnTimeSort.setIcon(counterclockwiseIcon); 176 | btnTimeSort.setToolTipText("照草稿最後編輯時間排序 (反向)"); 177 | } else { 178 | drafts = jySrtTools.getDrafts(new JyDraftLastModifiedTimeComparator(), false); 179 | btnTimeSort.setIcon(clockwiseIcon); 180 | btnTimeSort.setToolTipText("照草稿最後編輯時間排序"); 181 | } 182 | reloadList(drafts); 183 | }); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/FindReplaceDialog.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JySrtTools; 4 | import app.jackychu.jysrttools.Subtitle; 5 | import app.jackychu.jysrttools.exception.JySrtToolsException; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.awt.event.KeyEvent; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Locale; 13 | import java.util.Objects; 14 | 15 | public class FindReplaceDialog extends JDialog { 16 | private final JFrame parent; 17 | private final JTextField findTextField; 18 | private final JButton findButton; 19 | private final JButton clearButton; 20 | private final JButton replaceButton; 21 | private final JButton replaceAllButton; 22 | private final JPanel replacePanel; 23 | private final JTextField replaceTextField; 24 | private boolean replaceMode = false; 25 | 26 | public FindReplaceDialog(JFrame parent, boolean modal) { 27 | super(parent, modal); 28 | this.parent = parent; 29 | 30 | setLocationRelativeTo(null); 31 | Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); 32 | this.setLocation(dim.width / 2 - this.getSize().width / 2, dim.height / 2 - this.getSize().height / 2); 33 | setResizable(false); 34 | setLayout(new GridLayout(2, 1)); 35 | setTitle("尋找"); 36 | 37 | JPanel findPanel = new JPanel(); 38 | findPanel.setLayout(new BorderLayout()); 39 | findPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 0)); 40 | findPanel.add(new JLabel(" 尋找字串: "), BorderLayout.WEST); 41 | findTextField = new JTextField(); 42 | findTextField.setFont(findTextField.getFont().deriveFont(14f)); 43 | findTextField.setMinimumSize(new Dimension(200, 36)); 44 | findPanel.add(findTextField, BorderLayout.CENTER); 45 | findButton = new JButton("尋找"); 46 | clearButton = new JButton("清除尋找"); 47 | addButtonsListener(findButton); 48 | addButtonsListener(clearButton); 49 | JPanel buttonPanel1 = new JPanel(); 50 | buttonPanel1.setLayout(new GridLayout(1, 2)); 51 | buttonPanel1.add(findButton); 52 | buttonPanel1.add(clearButton); 53 | findPanel.add(buttonPanel1, BorderLayout.EAST); 54 | add(findPanel); 55 | 56 | replacePanel = new JPanel(); 57 | replacePanel.setPreferredSize(new Dimension(600, 36)); 58 | replacePanel.setLayout(new BorderLayout()); 59 | replacePanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 0)); 60 | replacePanel.add(new JLabel(" 替換字串: "), BorderLayout.WEST); 61 | replaceTextField = new JTextField(); 62 | replaceTextField.setFont(replaceTextField.getFont().deriveFont(14f)); 63 | replacePanel.add(replaceTextField, BorderLayout.CENTER); 64 | replaceButton = new JButton("替換"); 65 | replaceAllButton = new JButton("全部替換"); 66 | addButtonsListener(replaceButton); 67 | addButtonsListener(replaceAllButton); 68 | JPanel buttonPanel2 = new JPanel(); 69 | buttonPanel2.setLayout(new GridLayout(1, 2)); 70 | buttonPanel2.add(replaceButton); 71 | buttonPanel2.add(replaceAllButton); 72 | replacePanel.add(buttonPanel2, BorderLayout.EAST); 73 | add(replacePanel); 74 | replacePanel.setVisible(false); 75 | 76 | getRootPane().registerKeyboardAction(e -> this.setVisible(false), 77 | KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), 78 | JComponent.WHEN_IN_FOCUSED_WINDOW); 79 | 80 | String os = System.getProperty("os.name"); 81 | int cmdKey = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); //KeyEvent.VK_META; 82 | if (os.toLowerCase(Locale.ROOT).contains("windows")) { 83 | cmdKey = KeyEvent.CTRL_DOWN_MASK; 84 | } 85 | getRootPane().registerKeyboardAction(e -> this.setReplaceMode(false), 86 | KeyStroke.getKeyStroke(KeyEvent.VK_F, cmdKey), 87 | JComponent.WHEN_IN_FOCUSED_WINDOW); 88 | getRootPane().registerKeyboardAction(e -> this.setReplaceMode(true), 89 | KeyStroke.getKeyStroke(KeyEvent.VK_R, cmdKey), 90 | JComponent.WHEN_IN_FOCUSED_WINDOW); 91 | 92 | } 93 | 94 | public void setReplaceMode(boolean replaceMode) { 95 | replacePanel.setVisible(replaceMode); 96 | if (replaceMode) { 97 | this.replaceTextField.grabFocus(); 98 | this.setTitle("尋找 & 替換"); 99 | this.replaceMode = true; 100 | } else { 101 | this.findTextField.grabFocus(); 102 | this.setTitle("尋找"); 103 | this.replaceMode = false; 104 | } 105 | this.pack(); 106 | } 107 | 108 | private void addButtonsListener(JButton btn) { 109 | btn.setFocusable(false); 110 | btn.addActionListener(e -> { 111 | if (e.getSource() == clearButton) { 112 | findTextField.setText(""); 113 | replaceTextField.setText(""); 114 | } 115 | 116 | String str = findTextField.getText(); 117 | JySrtTools jySrtTools = (JySrtTools) parent; 118 | JTable tbl = jySrtTools.getJyTextPanel().getTextsPanel().getSubtitleTable(); 119 | int index = tbl.getSelectedRow(); 120 | 121 | List subtitles; 122 | int foundCount = 0; 123 | try { 124 | subtitles = jySrtTools.getCurrentSelectedDraft().getSubtitles(); 125 | int selectedIndex = tbl.getSelectedRow(); 126 | if (e.getSource() == replaceButton) { 127 | if (selectedIndex != -1 && !Objects.equals(str, "")) { 128 | Subtitle sub = subtitles.get(selectedIndex); 129 | sub.setText(sub.getText().replace(str, replaceTextField.getText())); 130 | sub.setFound(false); 131 | tbl.clearSelection(); 132 | jySrtTools.getJyTextPanel().getTextsPanel().saveSubtitleChanges(sub); 133 | } 134 | } else if (e.getSource() == replaceAllButton && selectedIndex != -1) { 135 | for (Subtitle sub : subtitles) { 136 | sub.setText(sub.getText().replace(str, replaceTextField.getText())); 137 | sub.setFound(false); 138 | jySrtTools.getJyTextPanel().getTextsPanel().saveSubtitleChanges(sub); 139 | } 140 | } 141 | 142 | List foundRows = new ArrayList<>(); 143 | for (Subtitle sub : subtitles) { 144 | sub.setFindingText(str); 145 | sub.setFound(sub.getText().contains(str)); 146 | if (sub.isFound()) { 147 | foundCount++; 148 | foundRows.add(sub.getNum() - 1); 149 | } 150 | } 151 | 152 | index = selectNextFoundRow(index, subtitles); 153 | jySrtTools.getJyTextPanel().getTextsPanel().setSubtitles(jySrtTools.getCurrentSelectedDraft()); 154 | selectedIndex = 0; 155 | if (index != -1 && !Objects.equals(str, "")) { 156 | tbl.setRowSelectionInterval(index, index); 157 | selectedIndex = foundRows.indexOf(index) + 1; 158 | tbl.scrollRectToVisible(tbl.getCellRect(index, 0, true)); 159 | } 160 | 161 | if (e.getSource() == clearButton) { 162 | if (this.replaceMode) { 163 | this.setTitle("尋找 & 替換"); 164 | } else { 165 | this.setTitle("尋找"); 166 | } 167 | } else { 168 | if (this.replaceMode) { 169 | this.setTitle("尋找 & 替換 - " + selectedIndex + "/" + foundCount); 170 | } else { 171 | this.setTitle("尋找 - " + selectedIndex + "/" + foundCount); 172 | } 173 | } 174 | } catch (JySrtToolsException ex) { 175 | JOptionPane.showMessageDialog(jySrtTools, 176 | new ErrorMessagePanel(ex), "無法尋找", JOptionPane.ERROR_MESSAGE); 177 | } 178 | }); 179 | } 180 | 181 | private int selectNextFoundRow(int index, List subtitles) { 182 | for (int i = index + 1; i < subtitles.size(); i++) { 183 | Subtitle sub = subtitles.get(i); 184 | if (sub.isFound()) { 185 | return i; 186 | } 187 | } 188 | 189 | return -1; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/ui/DraftActionPanel.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools.ui; 2 | 3 | import app.jackychu.jysrttools.JySrtTools; 4 | import app.jackychu.jysrttools.JyUtils; 5 | import app.jackychu.jysrttools.Subtitle; 6 | import app.jackychu.jysrttools.exception.JySrtToolsException; 7 | 8 | import javax.imageio.ImageIO; 9 | import javax.swing.*; 10 | import javax.swing.filechooser.FileNameExtensionFilter; 11 | import java.awt.*; 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Objects; 18 | 19 | public class DraftActionPanel extends JPanel { 20 | private final JySrtTools jySrtTools; 21 | private final JyTextPanel parent; 22 | private final Map buttons = new HashMap<>(); 23 | 24 | public DraftActionPanel(JySrtTools jySrtTools, JyTextPanel parent) { 25 | this.jySrtTools = jySrtTools; 26 | this.parent = parent; 27 | setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 20)); 28 | setLayout(new BorderLayout()); 29 | 30 | JLabel label = new JLabel("步驟三: 執行功能", JLabel.LEFT); 31 | 32 | JPanel panel = new JPanel(); 33 | JButton btnTcTranslate = new JButton("简体 轉換為 繁體"); 34 | // JButton btnTwTranslate = new JButton("简体 翻譯為 台灣正體"); 35 | JButton btnSrtExport = new JButton("輸出 SRT 檔"); 36 | JButton btnTxtExport = new JButton("輸出 txt 文字檔"); 37 | JButton btnRemove = new JButton("清除字幕"); 38 | JButton btnSrtImport = new JButton("載入 SRT 檔"); 39 | btnTcTranslate.setEnabled(false); 40 | // btnTwTranslate.setEnabled(false); 41 | btnSrtExport.setEnabled(false); 42 | btnTxtExport.setEnabled(false); 43 | btnRemove.setEnabled(false); 44 | btnSrtImport.setEnabled(false); 45 | buttons.put("tcTranslate", btnTcTranslate); 46 | // buttons.put("twTranslate", btnTwTranslate); 47 | buttons.put("srtExport", btnSrtExport); 48 | buttons.put("txtExport", btnTxtExport); 49 | buttons.put("remove", btnRemove); 50 | buttons.put("srtImport", btnSrtImport); 51 | setButtonActionListener("tcTranslate"); 52 | // setButtonActionListener("twTranslate"); 53 | setButtonActionListener("srtExport"); 54 | setButtonActionListener("txtExport"); 55 | setButtonActionListener("remove"); 56 | setButtonActionListener("srtImport"); 57 | panel.setLayout(new GridLayout(buttons.size(), 1)); 58 | panel.add(btnTcTranslate); 59 | // panel.add(btnTwTranslate); 60 | panel.add(btnSrtExport); 61 | panel.add(btnTxtExport); 62 | panel.add(btnRemove); 63 | panel.add(btnSrtImport); 64 | 65 | add(label, BorderLayout.NORTH); 66 | add(panel, BorderLayout.CENTER); 67 | } 68 | 69 | private void setButtonActionListener(String button) { 70 | switch (button) { 71 | case "tcTranslate": 72 | case "twTranslate": 73 | buttons.get(button).addActionListener(e -> { 74 | jySrtTools.getProgressDialog().doTranslate(button, jySrtTools.getCurrentSelectedDraft()); 75 | jySrtTools.getCurrentSelectedDraft().cleanSubtitles(); 76 | try { 77 | parent.getTextsPanel().setSubtitles(jySrtTools.getCurrentSelectedDraft()); 78 | } catch (JySrtToolsException jye) { 79 | parent.getActionPanel().enableButtons(false); 80 | JOptionPane.showMessageDialog(jySrtTools, 81 | new ErrorMessagePanel(jye), "草稿文字更新錯誤", JOptionPane.ERROR_MESSAGE); 82 | } 83 | }); 84 | break; 85 | case "txtExport": 86 | case "srtExport": 87 | buttons.get(button).addActionListener(e -> { 88 | String type = button.equals("txtExport") ? "txt" : "srt"; 89 | String typeName = button.equals("txtExport") ? "TXT 文字檔" : "SRT 字幕檔"; 90 | JFileChooser fileChooser = new JFileChooser(); 91 | fileChooser.setDialogTitle("請選擇要匯出的檔案目錄和名稱"); 92 | fileChooser.setSelectedFile(new File(jySrtTools.getCurrentSelectedDraft().getName() + "." + type)); 93 | fileChooser.setCurrentDirectory(new File(System.getProperty("user.home"))); 94 | fileChooser.setAcceptAllFileFilterUsed(false); 95 | FileNameExtensionFilter filter = new FileNameExtensionFilter(typeName, type); 96 | fileChooser.addChoosableFileFilter(filter); 97 | int result = fileChooser.showSaveDialog(jySrtTools); 98 | if (result == JFileChooser.APPROVE_OPTION) { 99 | File selectedFile = fileChooser.getSelectedFile(); 100 | String path = selectedFile.getAbsolutePath(); 101 | if (!path.endsWith("." + type)) { 102 | path += "." + type; 103 | } 104 | try { 105 | JyUtils.exportToFile(jySrtTools.getCurrentSelectedDraft(), path, type); 106 | } catch (JySrtToolsException jye) { 107 | JOptionPane.showMessageDialog(jySrtTools, 108 | new ErrorMessagePanel(jye), "檔案匯出失敗", JOptionPane.ERROR_MESSAGE); 109 | } 110 | } 111 | 112 | }); 113 | break; 114 | case "remove": 115 | buttons.get(button).addActionListener(e -> { 116 | Object[] options = {"清除", "算了"}; 117 | ImageIcon icon = null; 118 | try { 119 | Image image = ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/remove.png"))); 120 | icon = new ImageIcon(image); 121 | } catch (IOException ioe) { 122 | ioe.printStackTrace(); 123 | } 124 | 125 | int result = JOptionPane.showOptionDialog(jySrtTools, "確定清除草稿字幕?", "草稿字幕清除", 126 | JOptionPane.YES_NO_OPTION, 127 | JOptionPane.QUESTION_MESSAGE, 128 | icon, 129 | options, 130 | options[0]); 131 | if (result == JOptionPane.YES_OPTION) { 132 | try { 133 | jySrtTools.getCurrentSelectedDraft().deleteDraftSubtitles(); 134 | } catch (JySrtToolsException jye) { 135 | JOptionPane.showMessageDialog(jySrtTools, 136 | new ErrorMessagePanel(jye), "草稿字幕清除失敗", JOptionPane.ERROR_MESSAGE); 137 | } 138 | saveDraft(); 139 | } 140 | }); 141 | break; 142 | case "srtImport": 143 | buttons.get(button).addActionListener(e -> { 144 | JFileChooser fileChooser = new JFileChooser(); 145 | fileChooser.setDialogTitle("請選擇要載入的 SRT 檔案名稱"); 146 | fileChooser.setSelectedFile(new File(jySrtTools.getCurrentSelectedDraft().getName() + ".srt")); 147 | fileChooser.setCurrentDirectory(new File(System.getProperty("user.home"))); 148 | fileChooser.setAcceptAllFileFilterUsed(false); 149 | FileNameExtensionFilter filter = new FileNameExtensionFilter("SRT 字幕檔", "srt"); 150 | fileChooser.addChoosableFileFilter(filter); 151 | int result = fileChooser.showOpenDialog(jySrtTools); 152 | if (result == JFileChooser.APPROVE_OPTION) { 153 | File selectedFile = fileChooser.getSelectedFile(); 154 | String path = selectedFile.getAbsolutePath(); 155 | 156 | Object[] options = {"是", "算了"}; 157 | ImageIcon icon = null; 158 | try { 159 | Image image = ImageIO.read(Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("images/question.png"))); 160 | icon = new ImageIcon(image); 161 | } catch (IOException ioe) { 162 | ioe.printStackTrace(); 163 | } 164 | 165 | result = JOptionPane.showOptionDialog(jySrtTools, "是否要清除舊字幕並載入新字幕?", "草稿字幕清除", 166 | JOptionPane.YES_NO_OPTION, 167 | JOptionPane.QUESTION_MESSAGE, 168 | icon, 169 | options, 170 | options[0]); 171 | if (result == JOptionPane.YES_OPTION) { 172 | try { 173 | List subtitles = JyUtils.loadSrtFile(path); 174 | jySrtTools.getCurrentSelectedDraft().replaceDraftSubtitles(subtitles); 175 | } catch (JySrtToolsException ex) { 176 | JOptionPane.showMessageDialog(jySrtTools, 177 | new ErrorMessagePanel(ex), "SRT 檔載入失敗", JOptionPane.ERROR_MESSAGE); 178 | } 179 | saveDraft(); 180 | } 181 | 182 | } 183 | }); 184 | break; 185 | } 186 | } 187 | 188 | private void saveDraft() { 189 | try { 190 | JyUtils.saveDraft(jySrtTools.getCurrentSelectedDraft()); 191 | } catch (JySrtToolsException jye) { 192 | JOptionPane.showMessageDialog(jySrtTools, 193 | new ErrorMessagePanel(jye), "草稿更新存檔失敗", JOptionPane.ERROR_MESSAGE); 194 | } 195 | try { 196 | parent.getTextsPanel().setSubtitles(jySrtTools.getCurrentSelectedDraft()); 197 | } catch (JySrtToolsException jye) { 198 | JOptionPane.showMessageDialog(jySrtTools, 199 | new ErrorMessagePanel(jye), "草稿重新載入失敗", JOptionPane.ERROR_MESSAGE); 200 | } 201 | } 202 | 203 | public void enableButtons(boolean enable) { 204 | enableButtons(enable, false); 205 | } 206 | 207 | public void enableButtons(boolean enable, boolean reload) { 208 | for (JButton btn : buttons.values()) { 209 | if (btn.getText().contains("載入 SRT 檔") && !reload) { 210 | btn.setEnabled(true); 211 | continue; 212 | } 213 | btn.setEnabled(enable); 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/JyDraft.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import app.jackychu.jysrttools.exception.JySrtToolsException; 4 | import com.github.houbb.heaven.util.util.JsonUtil; 5 | import lombok.*; 6 | import org.apache.commons.text.StringSubstitutor; 7 | import org.json.simple.JSONArray; 8 | import org.json.simple.JSONObject; 9 | import org.json.simple.parser.JSONParser; 10 | import org.json.simple.parser.ParseException; 11 | 12 | import java.util.*; 13 | 14 | /** 15 | * Jianying video object 16 | */ 17 | @Data 18 | public class JyDraft { 19 | private String id; 20 | private String name; 21 | private JSONObject info; 22 | private String infoFilename; 23 | private String coverFilename; 24 | private String folderPath; 25 | private long lastModifiedTime; 26 | private boolean hidden = false; 27 | 28 | @Getter(AccessLevel.NONE) 29 | private List subtitles; 30 | 31 | @SneakyThrows 32 | public List getSubtitles() { 33 | if (this.subtitles == null) { 34 | this.subtitles = new ArrayList<>(); 35 | loadDraftSubtitles(); 36 | } 37 | return this.subtitles; 38 | } 39 | 40 | /** 41 | * Get draft subtitles 42 | * 43 | * @return List of subtitle (in time order) 44 | * @throws JySrtToolsException Parse draft info json error 45 | */ 46 | public List loadDraftSubtitles() throws JySrtToolsException { 47 | if (this.info == null) { 48 | loadJyDraftInfo(); 49 | } 50 | 51 | Map texts = new HashMap<>(); 52 | Map contentFormats = new HashMap<>(); 53 | JSONArray txts = (JSONArray) ((JSONObject) this.info.get("materials")).get("texts"); 54 | for (Object txt : txts.toArray()) { 55 | Object txtId = ((JSONObject) txt).get("id"); 56 | Object txtObj = ((JSONObject) txt).get("content"); 57 | Object txtType = ((JSONObject) txt).get("type"); 58 | if (txtType.toString().equals("subtitle")) { 59 | try { 60 | String originContent = txtObj.toString(); 61 | JSONObject content = (JSONObject) (new JSONParser()).parse(originContent); 62 | texts.put(txtId.toString(), content.get("text").toString()); 63 | content.put("text", "${text}"); 64 | contentFormats.put(txtId.toString(), content.toString()); 65 | } catch (ParseException e) { 66 | texts.put(txtId.toString(), e.toString()); 67 | } 68 | } 69 | } 70 | 71 | JSONArray tracks = (JSONArray) this.info.get("tracks"); 72 | for (Object track : tracks.toArray()) { 73 | JSONObject tk = (JSONObject) track; 74 | // only flag=2 and type=text is subtitle 75 | if (!((tk.get("flag").toString().equals("1") || tk.get("flag").toString().equals("2")) && 76 | tk.get("type").toString().equals("text"))) continue; 77 | JSONArray segments = (JSONArray) tk.get("segments"); 78 | for (Object segment : segments.toArray()) { 79 | String materialId = ((JSONObject) segment).get("material_id").toString(); 80 | if (texts.containsKey(materialId)) { 81 | Subtitle sub = new Subtitle(); 82 | sub.setId(materialId); 83 | sub.setText(texts.get(materialId)); 84 | sub.setContentFormat(contentFormats.get(materialId)); 85 | JSONObject target = (JSONObject) ((JSONObject) segment).get("target_timerange"); 86 | sub.setDuration(Long.parseLong(target.get("duration").toString())); 87 | sub.setStartTime(Long.parseLong(target.get("start").toString())); 88 | sub.setEndTime(sub.getStartTime() + sub.getDuration()); 89 | this.subtitles.add(sub); 90 | } 91 | } 92 | 93 | Collections.sort(this.subtitles); 94 | int index = 1; 95 | for (Subtitle sub : this.subtitles) { 96 | sub.setNum(index++); 97 | } 98 | } 99 | 100 | return subtitles; 101 | } 102 | 103 | /** 104 | * Load draft info from json file 105 | * 106 | * @return JSON object of draft info 107 | * @throws JySrtToolsException Load draft info file error 108 | */ 109 | public JSONObject loadJyDraftInfo() throws JySrtToolsException { 110 | try { 111 | this.info = JyUtils.loadJsonData(this.infoFilename); 112 | } catch (JySrtToolsException e) { 113 | throw new JySrtToolsException("讀取草稿內容錯誤! " + System.lineSeparator() + e.getMessage(), e); 114 | } 115 | 116 | return this.info; 117 | } 118 | 119 | /** 120 | * Update draft subtitle to info object 121 | * 122 | * @param subtitle New subtitles which want to update 123 | */ 124 | public void updateDraftSubtitle(Subtitle subtitle) { 125 | JSONArray originTexts; 126 | originTexts = (JSONArray) ((JSONObject) this.info.get("materials")).get("texts"); 127 | for (Object originText : originTexts.toArray()) { 128 | String id = ((JSONObject) originText).get("id").toString(); 129 | int index = this.subtitles.indexOf(new Subtitle(id)); 130 | if (index != -1){ 131 | Subtitle sub = this.subtitles.get(index); 132 | ((JSONObject) originText).put("content", sub.getFormattedText()); 133 | } 134 | } 135 | } 136 | 137 | /** 138 | * Delete draft subtitles 139 | * 140 | * @throws JySrtToolsException Load draft texts fail 141 | */ 142 | public void deleteDraftSubtitles() throws JySrtToolsException { 143 | if (this.subtitles == null || this.subtitles.isEmpty()) { 144 | loadDraftSubtitles(); 145 | } 146 | 147 | // Remove texts content 148 | JSONArray originTexts; 149 | originTexts = (JSONArray) ((JSONObject) this.info.get("materials")).get("texts"); 150 | for (Object originText : originTexts.toArray()) { 151 | if (((JSONObject) originText).get("type").toString().equals("subtitle")) { 152 | originTexts.remove(originText); 153 | } 154 | } 155 | 156 | // Remove tracks 157 | List extraMaterialIds = new ArrayList<>(); 158 | JSONArray tracks = (JSONArray) this.info.get("tracks"); 159 | for (Object track : tracks.toArray()) { 160 | JSONObject tk = (JSONObject) track; 161 | // only flag=2 and type=text is subtitle 162 | if (!((tk.get("flag").toString().equals("1") || tk.get("flag").toString().equals("2")) && 163 | tk.get("type").toString().equals("text"))) continue; 164 | JSONArray segments = (JSONArray) tk.get("segments"); 165 | for (Object segment : segments.toArray()) { 166 | String materialId = ((JSONObject) segment).get("material_id").toString(); 167 | if (this.subtitles.contains(new Subtitle(materialId))) { 168 | String extraMaterialId = null; 169 | if (((JSONObject) segment).containsKey("extra_material_refs")) { 170 | extraMaterialId = ((JSONArray) ((JSONObject) segment).get("extra_material_refs")).get(0).toString(); 171 | } else if (((JSONObject) segment).containsKey("extra_material_ids")) { 172 | extraMaterialId = ((JSONArray) ((JSONObject) segment).get("extra_material_ids")).get(0).toString(); 173 | } 174 | if (!Objects.isNull(extraMaterialId)) 175 | extraMaterialIds.add(extraMaterialId); 176 | segments.remove(segment); 177 | } 178 | } 179 | } 180 | 181 | // Remove extra materials 182 | JSONArray extraMaterials = (JSONArray) ((JSONObject) this.info.get("materials")).get("material_animations"); 183 | for (Object extra : extraMaterials.toArray()) { 184 | JSONObject ex = (JSONObject) extra; 185 | if (extraMaterialIds.contains(ex.get("id").toString())) { 186 | extraMaterials.remove(ex); 187 | } 188 | } 189 | 190 | this.subtitles = null; 191 | } 192 | 193 | /** 194 | * Replace/Import new subtitle instead exist one 195 | * 196 | * @param newSubtitles new subtitle list 197 | * @throws JySrtToolsException load subtitle error 198 | */ 199 | public void replaceDraftSubtitles(List newSubtitles) throws JySrtToolsException { 200 | deleteDraftSubtitles(); 201 | 202 | JSONParser parser = new JSONParser(); 203 | 204 | JSONArray originTexts = (JSONArray) ((JSONObject) this.info.get("materials")).get("texts"); 205 | JSONArray extraMaterials = (JSONArray) ((JSONObject) this.info.get("materials")).get("material_animations"); 206 | 207 | String trackTemp = DraftTemplates.getTemplate("draft_track"); 208 | Map values = new HashMap<>(); 209 | String trackId = UUID.randomUUID().toString().toUpperCase(); 210 | values.put("id", trackId); 211 | JSONObject trackObj; 212 | try { 213 | trackObj = (JSONObject) parser.parse(StringSubstitutor.replace(trackTemp, values, "${", "}")); 214 | } catch (ParseException e) { 215 | throw new JySrtToolsException("新增 SRT 字幕失敗 (Track) " + System.lineSeparator() + e.getMessage(), e); 216 | } 217 | JSONArray segments = (JSONArray) trackObj.get("segments"); 218 | 219 | for (Subtitle sub : newSubtitles) { 220 | String extraTemp = DraftTemplates.getTemplate("draft_extra_material"); 221 | values = new HashMap<>(); 222 | String extraId = UUID.randomUUID().toString().toUpperCase(); 223 | values.put("id", extraId); 224 | String extraStr = StringSubstitutor.replace(extraTemp, values, "${", "}"); 225 | try { 226 | JSONObject obj = (JSONObject) parser.parse(extraStr); 227 | extraMaterials.add(obj); 228 | } catch (ParseException e) { 229 | throw new JySrtToolsException("新增 SRT 字幕失敗 (Extra Material) " + System.lineSeparator() + e.getMessage(), e); 230 | } 231 | 232 | String textTemp = DraftTemplates.getTemplate("draft_text"); 233 | values = new HashMap<>(); 234 | values.put("font_path", DraftTemplates.getDefaultFontPath()); 235 | String textId = UUID.randomUUID().toString().toUpperCase(); 236 | values.put("id", textId); 237 | String textStr = StringSubstitutor.replace(textTemp, values, "${", "}"); 238 | try { 239 | JSONObject textObj = (JSONObject) parser.parse(textStr); 240 | textObj.put("content", sub.getFormattedText().replace("${font_path}", DraftTemplates.getDefaultFontPath())); 241 | originTexts.add(textObj); 242 | } catch (ParseException e) { 243 | throw new JySrtToolsException("新增 SRT 字幕失敗 (Text) " + System.lineSeparator() + e.getMessage(), e); 244 | } 245 | 246 | String segmentTemp = DraftTemplates.getTemplate("draft_segment"); 247 | values = new HashMap<>(); 248 | String segmentId = UUID.randomUUID().toString().toUpperCase(); 249 | values.put("id", segmentId); 250 | values.put("extra_material_refs", extraId); 251 | values.put("material_id", textId); 252 | values.put("duration", String.valueOf(sub.getDuration())); 253 | values.put("start", String.valueOf(sub.getStartTime())); 254 | String segmentStr = StringSubstitutor.replace(segmentTemp, values, "${", "}"); 255 | JSONObject segmentObj; 256 | try { 257 | segmentObj = (JSONObject) parser.parse(segmentStr); 258 | segments.add(segmentObj); 259 | } catch (ParseException e) { 260 | throw new JySrtToolsException("新增 SRT 字幕失敗 (Segment) " + System.lineSeparator() + e.getMessage(), e); 261 | } 262 | } 263 | JSONArray tracks = (JSONArray) this.info.get("tracks"); 264 | tracks.add(trackObj); 265 | 266 | this.subtitles = null; 267 | } 268 | 269 | /** 270 | * Clean current subtitles after translate or import 271 | */ 272 | public void cleanSubtitles() { 273 | this.subtitles = null; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/main/java/app/jackychu/jysrttools/JyUtils.java: -------------------------------------------------------------------------------- 1 | package app.jackychu.jysrttools; 2 | 3 | import app.jackychu.jysrttools.exception.JySrtToolsException; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.json.simple.JSONArray; 6 | import org.json.simple.JSONObject; 7 | import org.json.simple.parser.JSONParser; 8 | 9 | import java.io.*; 10 | import java.nio.charset.StandardCharsets; 11 | import java.nio.file.Path; 12 | import java.util.*; 13 | 14 | /** 15 | * Some utils for JY video 16 | */ 17 | public class JyUtils { 18 | final static String ROOT_DRAFT_META_INFO_FILENAME = "root_meta_info.json"; 19 | 20 | /** 21 | * Get jr-srt-tools version number 22 | * 23 | * @return version number 24 | */ 25 | public static String getVersion() { 26 | String version = "N/A"; 27 | try { 28 | InputStream input = JyUtils.class.getClassLoader().getResourceAsStream("config.properties"); 29 | Properties prop = new Properties(); 30 | prop.load(input); 31 | version = prop.getProperty("version"); 32 | } catch (IOException e2) { 33 | e2.printStackTrace(); 34 | } 35 | 36 | return version; 37 | } 38 | 39 | /** 40 | * Get JY version number 41 | * 42 | * @return JY version number 43 | */ 44 | public static String getJyVersion() { 45 | String jyVersion = "N/A"; 46 | 47 | String os = System.getProperty("os.name"); 48 | try { 49 | String[] cmd; 50 | if (os.toLowerCase(Locale.ROOT).contains("windows")) { 51 | // cmd = new String[]{"cmd.exe", "dir"}; 52 | InputStream input = JyUtils.class.getClassLoader().getResourceAsStream("config.properties"); 53 | Properties prop = new Properties(); 54 | prop.load(input); 55 | return prop.getProperty("jy_version"); 56 | } else { 57 | cmd = new String[]{"/usr/bin/bash", "mdls -name kMDItemVersion /Applications/VideoFusion-macOS.app | awk -F'\"' '{print $2}'"}; 58 | } 59 | Process process = new ProcessBuilder(cmd) 60 | .redirectErrorStream(true) 61 | .start(); 62 | 63 | ArrayList output = new ArrayList<>(); 64 | BufferedReader br = new BufferedReader( 65 | new InputStreamReader(process.getInputStream())); 66 | String line; 67 | while ((line = br.readLine()) != null) 68 | output.add(line); 69 | 70 | //There should really be a timeout here. 71 | if (0 != process.waitFor()) { 72 | jyVersion = "N/A"; 73 | } 74 | 75 | jyVersion = output.toString(); 76 | } catch (IOException | InterruptedException e) { 77 | try { 78 | InputStream input = JyUtils.class.getClassLoader().getResourceAsStream("config.properties"); 79 | Properties prop = new Properties(); 80 | prop.load(input); 81 | jyVersion = prop.getProperty("jy_version"); 82 | } catch (IOException e2) { 83 | e2.printStackTrace(); 84 | } 85 | } 86 | 87 | return jyVersion; 88 | } 89 | 90 | /** 91 | * Get JY video config path by OS (Mac, Windows) 92 | * 93 | * @return JY draft path 94 | */ 95 | public static String getPath() { 96 | String os = System.getProperty("os.name"); 97 | String home = System.getProperty("user.home"); 98 | if (os.toLowerCase(Locale.ROOT).contains("windows")) 99 | return home + "\\AppData\\Local\\JianyingPro\\User Data\\Projects\\com.lveditor.draft"; 100 | else 101 | return home + "/Library/Containers/com.lemon.lvpro/Data/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"; 102 | } 103 | 104 | /** 105 | * Get JianyingPro fonts path 106 | * 107 | * @return JianyingPro fonts path 108 | */ 109 | public static String getJyFontsPath() { 110 | String os = System.getProperty("os.name"); 111 | String home = System.getProperty("user.home"); 112 | if (os.toLowerCase(Locale.ROOT).contains("windows")) { 113 | return home + "\\AppData\\Local\\JianyingPro\\User Data\\Resources\\Font"; 114 | } else { 115 | return "/Applications/VideoFusion-macOS.app/Contents/Resources/Font"; 116 | } 117 | } 118 | 119 | /** 120 | * Find JianyingPro app location. 121 | * But it is too slow. The find command taks 30 seconds. 122 | * 123 | * @return JianyingPro app path 124 | * @throws JySrtToolsException Cannot find JianyingPro app 125 | */ 126 | public static String findJyAppPath() throws JySrtToolsException { 127 | String appName = "VideoFusion-macOS.app"; 128 | String findCmd = "find / -name %s"; 129 | String appPath = null; 130 | 131 | String os = System.getProperty("os.name"); 132 | if (os.toLowerCase(Locale.ROOT).contains("windows")) { 133 | appName = ""; 134 | findCmd = ""; 135 | } 136 | 137 | try { 138 | System.out.printf(findCmd + "%n", appName); 139 | Process process = new ProcessBuilder("find", "/", "-name", appName).start(); 140 | BufferedReader reader = new BufferedReader( 141 | new InputStreamReader(process.getInputStream())); 142 | 143 | String line; 144 | while ((line = reader.readLine()) != null) { 145 | appPath = line; 146 | } 147 | reader.close(); 148 | 149 | if (appPath == null) { 150 | throw new JySrtToolsException("無法找到剪映主程式!"); 151 | } 152 | 153 | return appPath; 154 | } catch (IOException e) { 155 | throw new JySrtToolsException("無法找到剪映主程式! " + System.lineSeparator() + e.getMessage(), e); 156 | } 157 | } 158 | 159 | /** 160 | * Get all draft projects 161 | * 162 | * @return All drafts name and its fold path 163 | * @throws JySrtToolsException Parse json file error 164 | */ 165 | public static List getAllJyDrafts() throws JySrtToolsException { 166 | List drafts = new ArrayList<>(); 167 | JSONObject draftJson = loadJsonData( 168 | String.valueOf(Path.of(getPath(), ROOT_DRAFT_META_INFO_FILENAME).toAbsolutePath())); 169 | JSONArray allDraftStore = (JSONArray) draftJson.get("all_draft_store"); 170 | for (Object obj : allDraftStore) { 171 | JSONObject draft = (JSONObject) obj; 172 | 173 | JyDraft jyDraft = new JyDraft(); 174 | jyDraft.setName(draft.get("draft_name").toString()); 175 | jyDraft.setId(draft.get("draft_id").toString()); 176 | jyDraft.setFolderPath(draft.get("draft_fold_path").toString()); 177 | jyDraft.setInfoFilename(draft.get("draft_json_file").toString()); 178 | jyDraft.setCoverFilename(draft.get("draft_cover").toString()); 179 | jyDraft.setLastModifiedTime(Long.parseLong(draft.get("tm_draft_modified").toString())); 180 | 181 | drafts.add(jyDraft); 182 | } 183 | 184 | return drafts; 185 | } 186 | 187 | /** 188 | * Load JianyingPro draft data fronm json file 189 | * 190 | * @param filename json filename 191 | * @return JSON object 192 | * @throws JySrtToolsException Open/parse json file error 193 | */ 194 | public static JSONObject loadJsonData(String filename) throws JySrtToolsException { 195 | if (!(new File(filename)).exists()) { 196 | throw new JySrtToolsException("開啟草稿檔案錯誤! " + System.lineSeparator() + "請新增剪映草稿後,再使用工具箱!"); 197 | } 198 | 199 | JSONParser jsonParser = new JSONParser(); 200 | FileReader reader; 201 | try { 202 | reader = new FileReader(filename, StandardCharsets.UTF_8); 203 | } catch (Exception e) { 204 | throw new JySrtToolsException("開啟草稿檔案錯誤! " + System.lineSeparator() + e.getMessage(), e); 205 | } 206 | 207 | JSONObject json; 208 | try { 209 | json = (JSONObject) jsonParser.parse(reader); 210 | } catch (Exception e) { 211 | throw new JySrtToolsException("分析草稿內容錯誤! " + System.lineSeparator() + e.getMessage(), e); 212 | } 213 | 214 | return json; 215 | } 216 | 217 | /** 218 | * Get JianyingPro draft subtitles in SRT format 219 | * 220 | * @param draft JianyingPro draft object 221 | * @throws JySrtToolsException get SRT format fail 222 | */ 223 | public static String getDraftSubtitlesSRT(JyDraft draft) throws JySrtToolsException { 224 | List subtitles = draft.getSubtitles(); 225 | StringBuilder srt = new StringBuilder(); 226 | 227 | for (Subtitle sub : subtitles) { 228 | // fix time overlap 229 | if (sub.getNum() > 1) { 230 | Subtitle preSub = subtitles.get(sub.getNum() - 2); // list index start from 0 231 | if (sub.getStartTime() < preSub.getEndTime()) { 232 | sub.setStartTime(preSub.getEndTime()); 233 | } 234 | } 235 | 236 | srt.append(sub.getNum()) 237 | .append("\n") 238 | .append(Subtitle.msToTimeStr(sub.getStartTime())) 239 | .append(" --> ") 240 | .append(Subtitle.msToTimeStr(sub.getEndTime())) 241 | .append("\n") 242 | .append(sub.getText()) 243 | .append("\n\n"); 244 | } 245 | 246 | return srt.toString(); 247 | } 248 | 249 | /** 250 | * Get JianyingPro draft subtitles in txt format 251 | * 252 | * @param draft JianyingPro draft object 253 | * @throws JySrtToolsException get TXT format fail 254 | */ 255 | public static String getDraftSubtitlesTxt(JyDraft draft) throws JySrtToolsException { 256 | StringBuilder sb = new StringBuilder(); 257 | for (Subtitle sub : draft.getSubtitles()) { 258 | sb.append(sub.getText()).append(System.lineSeparator()); 259 | } 260 | return sb.toString(); 261 | } 262 | 263 | /** 264 | * Save JyDraft object back to JianyingPro draft info json file 265 | * 266 | * @param draft JyDraft object 267 | * @throws JySrtToolsException save draft info json file fail 268 | */ 269 | public static void saveDraft(JyDraft draft) throws JySrtToolsException { 270 | try (FileWriter file = new FileWriter(draft.getInfoFilename(), StandardCharsets.UTF_8)) { 271 | file.write(draft.getInfo().toJSONString()); 272 | } catch (IOException e) { 273 | throw new JySrtToolsException("儲存草稿資料錯誤! " + draft.getName() + System.lineSeparator() + e.getMessage(), e); 274 | } 275 | } 276 | 277 | /** 278 | * Export JianyingPro draft subtitles to SRT file 279 | * 280 | * @param draft JyDraft object 281 | * @param filename SRT filename 282 | * @throws JySrtToolsException Export SRT file fail. 283 | */ 284 | public static void exportToFile(JyDraft draft, String filename, String type) throws JySrtToolsException { 285 | String data; 286 | if (type.equals("srt")) { 287 | data = getDraftSubtitlesSRT(draft); 288 | } else { // txt 289 | data = getDraftSubtitlesTxt(draft); 290 | } 291 | try (FileWriter file = new FileWriter(filename, StandardCharsets.UTF_8)) { 292 | file.write(data); 293 | } catch (IOException e) { 294 | throw new JySrtToolsException("匯出 " + type + " 檔案錯誤! " + draft.getName() + System.lineSeparator() + e.getMessage(), e); 295 | } 296 | } 297 | 298 | /** 299 | * Get all JianyingPro fonts 300 | * 301 | * @return JianyingPro font list 302 | * @throws JySrtToolsException Load font error 303 | */ 304 | public static List getAllJyFonts() throws JySrtToolsException { 305 | File[] files = new File(getJyFontsPath()).listFiles((f, name) -> name.endsWith(".ttf") || name.endsWith(".otf")); 306 | Arrays.sort(Objects.requireNonNull(files), (a, b) -> -a.getName().compareTo(b.getName())); 307 | 308 | // The bak file name is original_fontname.replaced_fontname.bak 309 | String[] bak = new File(getJyFontsPath()).list((f, name) -> name.endsWith(".bak")); 310 | Map bakFiles = new HashMap<>(); 311 | for (String b : Objects.requireNonNull(bak)) { 312 | String fontName = b.split("\\.")[0]; 313 | String replacedName = b.split("\\.")[1]; 314 | bakFiles.put(fontName, replacedName); 315 | } 316 | Set bakList = bakFiles.keySet(); 317 | 318 | List fonts = new ArrayList<>(); 319 | for (File f : Objects.requireNonNull(files)) { 320 | String name = f.getName().split("\\.")[0]; 321 | if (bakList.contains(name)) { 322 | fonts.add(new JyFont(f, bakFiles.get(name))); 323 | } else { 324 | fonts.add(new JyFont(f)); 325 | } 326 | } 327 | 328 | return fonts; 329 | } 330 | 331 | /** 332 | * Load subtitles from srt file to JY draft 333 | * 334 | * @param filename SRT file name 335 | * @return subtitle lise 336 | * @throws JySrtToolsException load srt file error 337 | */ 338 | public static List loadSrtFile(String filename) throws JySrtToolsException { 339 | JSONParser jsonParser = new JSONParser(); 340 | FileReader reader; 341 | try { 342 | reader = new FileReader(filename, StandardCharsets.UTF_8); 343 | } catch (Exception e) { 344 | throw new JySrtToolsException("開啟 SRT 檔案錯誤! " + System.lineSeparator() + e.getMessage(), e); 345 | } 346 | 347 | Subtitle sub = null; 348 | int subLineCount = 1; 349 | List subtitles = new ArrayList<>(); 350 | String format = DraftTemplates.getDefaultSubtitleFormat(); 351 | try (BufferedReader br = new BufferedReader(reader)) { 352 | for (String line; (line = br.readLine()) != null; ) { 353 | 354 | // some srt file with wrong encoding (UTF-8 BOM). 355 | // This is how to remove extra character which not belong to UTF-8 356 | line = line.replace("\uFEFF", ""); 357 | int num = StringUtils.isNumeric(line) ? Integer.parseInt(line) : -1; 358 | if (num != -1) { //number 359 | sub = new Subtitle(); 360 | sub.setContentFormat(format); 361 | subLineCount = 1; 362 | sub.setNum(num); 363 | } else { 364 | if (subLineCount == 1) { //time 365 | subLineCount = 2; 366 | String[] time = line.trim().split(" --> "); 367 | sub.setStartTime(Subtitle.timeStrToMs(time[0])); 368 | sub.setEndTime(Subtitle.timeStrToMs(time[1])); 369 | sub.setDuration(sub.getEndTime() - sub.getStartTime()); 370 | } else if (subLineCount == 2) { //text 371 | sub.setText(line.trim()); 372 | subtitles.add(sub); 373 | subLineCount = 3; 374 | } else { // multiline text 375 | if (!line.trim().equals("")) { 376 | sub.setText(sub.getText() + System.lineSeparator() + line.trim()); 377 | subLineCount++; 378 | } 379 | } 380 | } 381 | } 382 | 383 | return subtitles; 384 | } catch (IOException | NullPointerException e) { 385 | throw new JySrtToolsException("解析 SRT 檔案錯誤! " + System.lineSeparator() + e.getMessage(), e); 386 | } 387 | } 388 | 389 | } --------------------------------------------------------------------------------