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