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