|
1 | 1 | import "pkg:/source/api/baserequest.bs" |
| 2 | +import "pkg:/source/enums/CaptionMoveDirection.bs" |
2 | 3 | import "pkg:/source/enums/MediaPlaybackState.bs" |
| 4 | +import "pkg:/source/enums/String.bs" |
3 | 5 | import "pkg:/source/utils/config.bs" |
4 | 6 |
|
5 | 7 | sub init() |
@@ -69,58 +71,173 @@ end function |
69 | 71 | function newLayoutGroup(labels) |
70 | 72 | newlg = CreateObject("roSGNode", "LayoutGroup") |
71 | 73 | newlg.appendchildren(labels) |
72 | | - newlg.horizalignment = "center" |
73 | | - newlg.vertalignment = "bottom" |
| 74 | + newlg.layoutDirection = "vert" |
| 75 | + newlg.horizalignment = "left" |
| 76 | + newlg.vertalignment = "top" |
| 77 | + newlg.itemSpacings = "[5]" |
74 | 78 | return newlg |
75 | 79 | end function |
76 | 80 |
|
77 | | -function newRect(lg) |
78 | | - rectLG = CreateObject("roSGNode", "LayoutGroup") |
| 81 | +function newRect(lg, id as string) |
79 | 82 | rectxy = lg.BoundingRect() |
| 83 | + |
80 | 84 | rect = CreateObject("roSGNode", "Rectangle") |
| 85 | + rect.addreplace("id", id) |
| 86 | + rect.appendchild(lg) |
| 87 | + lg.translation = [5, 5] |
| 88 | + |
81 | 89 | rect.color = m.bgColor |
82 | 90 | rect.opacity = m.bgOpac |
83 | | - rect.width = rectxy.width + 50 |
84 | | - rect.height = rectxy.height |
| 91 | + rect.width = rectxy.width + 10 |
| 92 | + rect.height = rectxy.height + 10 |
| 93 | + |
85 | 94 | if lg.getchildCount() = 0 |
86 | 95 | rect.width = 0 |
87 | 96 | rect.height = 0 |
88 | 97 | end if |
89 | | - rectLG.translation = [0, -rect.height / 2] |
90 | | - rectLG.horizalignment = "center" |
91 | | - rectLG.vertalignment = "center" |
92 | | - rectLG.appendchild(rect) |
93 | | - return rectLG |
| 98 | + |
| 99 | + return rect |
94 | 100 | end function |
95 | 101 |
|
96 | 102 |
|
97 | 103 | sub updateCaption() |
98 | | - m.top.currentCaption = [] |
99 | | - if LCase(m.top.playerState) = "playingon" |
100 | | - m.top.currentPos = m.top.currentPos + 100 |
101 | | - texts = [] |
102 | | - captionStyles = {} |
103 | | - for each entry in m.captionList |
104 | | - if entry["start"] <= m.top.currentPos and m.top.currentPos < entry["end"] |
105 | | - captionStyles = entry["styles"] |
106 | | - t = m.tags.replaceAll(entry["text"], "") |
107 | | - texts.push(t) |
108 | | - end if |
109 | | - end for |
110 | | - labels = [] |
111 | | - for each text in texts |
112 | | - labels.push(newlabel (text)) |
113 | | - end for |
114 | | - lines = newLayoutGroup(labels) |
115 | | - rect = newRect(lines) |
116 | | - m.top.currentCaption = [rect, lines] |
117 | | - |
118 | | - m.top.captionStyles = captionStyles |
119 | | - else if LCase(m.top.playerState.right(1)) = "w" |
| 104 | + if LCase(m.top.playerState.right(1)) = "w" |
120 | 105 | m.top.playerState = m.top.playerState.left(len (m.top.playerState) - 1) |
| 106 | + return |
121 | 107 | end if |
| 108 | + |
| 109 | + m.top.currentCaption = [] |
| 110 | + |
| 111 | + if not isStringEqual(m.top.playerState, "playingon") then return |
| 112 | + |
| 113 | + m.top.currentPos = m.top.currentPos + 100 |
| 114 | + captions = [] |
| 115 | + |
| 116 | + for each entry in m.captionList |
| 117 | + if entry["start"] <= m.top.currentPos and m.top.currentPos < entry["end"] |
| 118 | + |
| 119 | + labels = [] |
| 120 | + for each text in entry["text"] |
| 121 | + labels.push(newlabel(text)) |
| 122 | + end for |
| 123 | + |
| 124 | + lines = newLayoutGroup(labels) |
| 125 | + rect = newRect(lines, entry.LookupCI("id")) |
| 126 | + |
| 127 | + finalStyles = prepareStyles(entry["styles"], rect, labels.count()) |
| 128 | + rect.translation = finalStyles.translation |
| 129 | + |
| 130 | + adjustForCaptionOverlaps(rect, captions) |
| 131 | + |
| 132 | + captions.push(rect) |
| 133 | + end if |
| 134 | + end for |
| 135 | + |
| 136 | + m.top.currentCaption = captions |
122 | 137 | end sub |
123 | 138 |
|
| 139 | +sub adjustForCaptionOverlaps(newCaption, existingCaptions, moveDirection = CaptionMoveDirection.UP) |
| 140 | + newCaptionBounds = newCaption.sceneBoundingRect() |
| 141 | + |
| 142 | + for i = existingCaptions.count() - 1 to 0 step -1 |
| 143 | + comparisonCaption = existingCaptions[i] |
| 144 | + |
| 145 | + ' Don't compare the caption to itself |
| 146 | + if isStringEqual(newCaption.lookupCI("id"), comparisonCaption.lookupCI("id")) then continue for |
| 147 | + |
| 148 | + comparisonCaptionBounds = comparisonCaption.sceneBoundingRect() |
| 149 | + |
| 150 | + overlapping = isOverlapping(newCaption, comparisonCaption) |
| 151 | + |
| 152 | + if overlapping |
| 153 | + ' If we're in the top half of the screen, push the other caption down |
| 154 | + ' otherwise push the other caption up |
| 155 | + if newCaptionBounds.y < 540 |
| 156 | + moveDirection = CaptionMoveDirection.DOWN |
| 157 | + end if |
| 158 | + |
| 159 | + if moveDirection = CaptionMoveDirection.DOWN |
| 160 | + calculatedVerticalPosition = newCaptionBounds.y + newCaptionBounds.height + 5 |
| 161 | + else |
| 162 | + calculatedVerticalPosition = newCaptionBounds.y - comparisonCaptionBounds.height - 5 |
| 163 | + end if |
| 164 | + |
| 165 | + if calculatedVerticalPosition < 0 then calculatedVerticalPosition = 0 |
| 166 | + |
| 167 | + comparisonCaption.translation = [comparisonCaption.translation[0], calculatedVerticalPosition] |
| 168 | + adjustForCaptionOverlaps(comparisonCaption, existingCaptions, moveDirection) |
| 169 | + end if |
| 170 | + end for |
| 171 | +end sub |
| 172 | + |
| 173 | +function isOverlapping(node1, node2) as boolean |
| 174 | + node1Bounds = node1.sceneBoundingRect() |
| 175 | + node2Bounds = node2.sceneBoundingRect() |
| 176 | + return node1Bounds.x < (node2Bounds.x + node2Bounds.width) and node2Bounds.x < (node1Bounds.x + node1Bounds.width) and node1Bounds.y < (node2Bounds.y + node2Bounds.height) and node2Bounds.y < (node1Bounds.y + node1Bounds.height) |
| 177 | +end function |
| 178 | + |
| 179 | +function prepareStyles(styles as object, rect, lineCount = 1 as integer) as object |
| 180 | + captionStyles = styles |
| 181 | + finalStyles = { translation: [960, 1020] } |
| 182 | + |
| 183 | + verticalPosition = 1020 |
| 184 | + horizontalPositon = 960 - (rect.BoundingRect().width / 2) |
| 185 | + |
| 186 | + if not isValidAndNotEmpty(captionStyles) |
| 187 | + return finalStyles |
| 188 | + end if |
| 189 | + |
| 190 | + captionLine = chainLookupReturn(captionStyles, "line", string.EMPTY) |
| 191 | + |
| 192 | + ' Line |
| 193 | + if not isStringEqual(captionLine, string.EMPTY) |
| 194 | + captionLineHeight = (rect.BoundingRect().height / lineCount) |
| 195 | + verticalPosition = processLineCue(captionLine, captionLineHeight) |
| 196 | + end if |
| 197 | + |
| 198 | + finalStyles.translation = [horizontalPositon, verticalPosition] |
| 199 | + |
| 200 | + return finalStyles |
| 201 | +end function |
| 202 | + |
| 203 | +function processLineCue(captionLine as string, captionLineHeight as float) as integer |
| 204 | + ' A percentage |
| 205 | + if captionLine.Instr("%") <> -1 |
| 206 | + calculatedPosition = 1080 * (val(captionLine) / 100) |
| 207 | + |
| 208 | + if calculatedPosition + captionLineHeight > 1080 |
| 209 | + calculatedPosition = 1080 - captionLineHeight |
| 210 | + end if |
| 211 | + |
| 212 | + return calculatedPosition |
| 213 | + end if |
| 214 | + |
| 215 | + ' Note: Non-numeric values are treated as 0 |
| 216 | + lineValue = val(captionLine) |
| 217 | + |
| 218 | + if lineValue = 0 then return 0 |
| 219 | + |
| 220 | + ' A positive line value. Count from the top |
| 221 | + if lineValue >= 0 |
| 222 | + calculatedPosition = lineValue * captionLineHeight |
| 223 | + |
| 224 | + if calculatedPosition + captionLineHeight > 1080 |
| 225 | + calculatedPosition = 1080 - captionLineHeight |
| 226 | + end if |
| 227 | + |
| 228 | + return calculatedPosition |
| 229 | + end if |
| 230 | + |
| 231 | + ' A negative line value. Count from the bottom |
| 232 | + calculatedPosition = 1080 - Abs(lineValue * captionLineHeight) |
| 233 | + |
| 234 | + if calculatedPosition < 0 |
| 235 | + calculatedPosition = 0 |
| 236 | + end if |
| 237 | + |
| 238 | + return calculatedPosition |
| 239 | +end function |
| 240 | + |
124 | 241 | function isTime(text) |
125 | 242 | return text.right(1) = chr(31) |
126 | 243 | end function |
@@ -158,20 +275,52 @@ function parseVTT(lines) |
158 | 275 | curEnd = -1 |
159 | 276 | entries = [] |
160 | 277 |
|
| 278 | + currentLine = [] |
| 279 | + entry = {} |
| 280 | + |
| 281 | + isNewLine = false |
| 282 | + |
161 | 283 | for i = 0 to lines.count() - 1 |
162 | 284 | if isTime(lines[i]) |
| 285 | + currentLine = [] |
| 286 | + |
163 | 287 | curStart = toMs(lines[i]) |
164 | 288 | lineData = extractStyles(lines[i + 1]) |
165 | 289 | curEnd = toMs(lineData.LookupCI("endTimestamp")) |
166 | 290 | curstyles = chainLookup(lineData, "styles") |
| 291 | + entry = { "start": curStart, "end": curEnd, "styles": curstyles, "id": `caption${i}` } |
| 292 | + isNewLine = true |
| 293 | + |
167 | 294 | i += 1 |
168 | | - else if curStart <> -1 |
| 295 | + continue for |
| 296 | + end if |
| 297 | + |
| 298 | + if isNewLine |
169 | 299 | trimmed = lines[i].trim() |
170 | | - if trimmed <> chr(0) |
171 | | - entry = { "start": curStart, "end": curEnd, "text": trimmed, "styles": curstyles } |
| 300 | + finalText = m.tags.replaceAll(trimmed, "") |
| 301 | + |
| 302 | + ' We reached a blank line |
| 303 | + if isStringEqual(finalText, string.EMPTY) |
| 304 | + entry.AddReplace("text", currentLine) |
172 | 305 | entries.push(entry) |
| 306 | + isNewLine = false |
| 307 | + |
| 308 | + continue for |
173 | 309 | end if |
| 310 | + |
| 311 | + ' We're inside a text block |
| 312 | + currentLine.push(finalText) |
| 313 | + |
| 314 | + ' We reached the end of the file and it doesn't end with a blank line |
| 315 | + if i = (lines.count() - 1) |
| 316 | + entry.AddReplace("text", currentLine) |
| 317 | + entries.push(entry) |
| 318 | + |
| 319 | + exit for |
| 320 | + end if |
| 321 | + |
174 | 322 | end if |
175 | 323 | end for |
| 324 | + |
176 | 325 | return entries |
177 | 326 | end function |
0 commit comments