-
Notifications
You must be signed in to change notification settings - Fork 6
/
arch.NoteBrowser.moon
306 lines (255 loc) · 10.9 KB
/
arch.NoteBrowser.moon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
export script_name = "Note Browser"
export script_description = "Loads a set of timestamped notes and adds options to mark them or jump between them."
export script_version = "1.3.6"
export script_namespace = "arch.NoteBrowser"
export script_author = "arch1t3cht"
-- This script allows loading a collection of subtitle QC notes prefixed by timestamps,
-- and allows navigation between mentioned lines in Aegisub.
-- It's able to add the notes themselves to the lines, but it can also simply highlight
-- the notes mentioned in the timestamps. Depending on the format of the notes, it could
-- either be helpful to see them directly in Aegisub, or it could be too hard to navigate.
-- In the latter case, the script could still save the time required to switch back and
-- forth between Aegisub and the notes file and scroll to the mentioned line.
--
-- Note format:
-- A note is any line starting with a timestamp of the form hh:mm:ss or mm:ss .
-- (As a consequence, lines starting with timestamps like 00:01:02.34 including centiseconds
-- will also be recognized as a note, however the centiseconds will be ignored.)
-- A note's text can be broken into multiple lines by indenting the following lines.
--
-- More precisely, a note's text consists of all the following lines up until the first blank line
-- with the property that the previous line is not indented.
--
-- A file of notes can be organized into different sections (say, collecting notes on different
-- topics or from different authors - the latter being the motivation for the macro names).
-- A section is started by a line of the form [<section_name>], where the
-- section's name <section_name> must not contain a closing bracket.
--
-- Any text not matching one of these two formats is skipped.
--
-- Furthermore, mpvQC files are transparently converted to the above format, provided the header is also included.
--
-- Example:
--
-- 0:01 - General note 1
-- More explanation for that note
--
-- Even more explanation
-- 1:50 - General note 2
--
-- [TLC]
-- 3:24 - yabai should be translated as "terrible" instead.
--
-- [Timing]
-- 1:55 - Scene bleed
-- 2:10 - Flashing subs
--
-- Most of the script's functions should be self-explanatory with this.
-- The one tricky element is "Jump to next/previous note by the same author":
-- This will jump to the next note whose author is *the author of the last note
-- the user jumped to using the ordinary "Jump to next/previous note" command.
-- While this may seem counter-intuitive, this ensures that successively using
-- "Jump to next/previous note by author" will indeed always jump to notes of the
-- same author, even after arriving at a subtitle line with multiple notes on it.
default_config = {
mark: false,
}
haveDepCtrl, DependencyControl = pcall(require, "l0.DependencyControl")
local config
local fun
local depctrl
local clipboard
if haveDepCtrl
depctrl = DependencyControl({
feed: "https://raw.githubusercontent.com/TypesettingTools/arch1t3cht-Aegisub-Scripts/main/DependencyControl.json",
{
{"l0.Functional", version: "0.6.0", url: "https://github.com/TypesettingTools/Functional",
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"},
"aegisub.clipboard"
}
})
config = depctrl\getConfigHandler(default_config, "config", false)
fun, clipboard = depctrl\requireModules!
else
id = () -> nil
config = {c: default_config, load: id, write: id}
fun = require "l0.Functional"
clipboard = require "aegisub.clipboard"
current_notes = {}
notes_owners = {}
current_all_notes = {}
local current_author
clear_markers = (subs) ->
for si, line in ipairs(subs)
continue if line.class != "dialogue"
line.text = line.text\gsub("{|QC|[^}]+}", "")\gsub("- |QC|[^|]+|", "- OG")
line.effect = line.effect\gsub("%[QC%-[^%]]*%]", "")
subs[si] = line
index_of_closest = (times, ms) ->
local closest
mindist = 15000
for si, time in pairs(times)
continue if time == nil
diff = math.abs(time - ms)
if diff < mindist
mindist = diff
closest = si
return closest
-- Joins lines with subsequent lines, until encountering a new line following an unindented line
join_lines = (notelines) ->
joined_lines = {}
local currentline
local lastindent
for line in *notelines
if currentline == nil
currentline = line
lastindent = ""
else
if line\match("^[%d]+:[%d]+") or line\match("^%[") or (line\match("^[%s]*$") and lastindent == "")
table.insert(joined_lines, currentline)
currentline = line
lastindent = ""
elseif not currentline\match("^[%s]*$")
currentline ..= "\\N" .. line\gsub("^[%s]*", "")
lastindent = currentline\match("^[%s]*")
table.insert(joined_lines, currentline) unless currentline == nil
return joined_lines
patch_for_mpvqc = (lines) ->
return lines unless #[true for line in *lines when line\match "^generator.*mpvQC"] > 0
patched_lines = {}
for line in *lines
if line\match "^%[[%d:]+%]"
section_header = line\match "^%[[^%]]+%] %[([^%]]+)%].*"
qc = line\gsub("^%[([%d:]+)%] %[[^%]]-%](.*)$", "%1 -%2")
table.insert(patched_lines, "[#{section_header}]")
table.insert(patched_lines, qc)
return patched_lines
fetch_note_from_clipboard = ->
note = clipboard.get!
if note\match "%d+:%d+"
return note
return ""
load_notes = (subs) ->
config\load()
btn, result = aegisub.dialog.display({{
class: "label",
label: "Paste your QC notes here: ",
x: 0, y: 0, width: 2, height: 1,
},{
class: "checkbox",
name: "mark",
value: config.c.mark,
label: "Mark lines with notes",
x: 0, y: 1, width: 1, height: 1,
},{
class: "checkbox",
name: "inline",
value: config.c.inline,
label: "Show notes in line",
x: 1, y: 1, width: 1, height: 1,
},{
class: "textbox",
name: "notes",
text: fetch_note_from_clipboard!,
x: 0, y: 2, width: 2, height: 10,
}})
return if not btn
notes = result.notes\gsub("\r\n", "\n")
notelines = fun.string.split notes, "\n"
notelines = patch_for_mpvqc notelines
notelines = join_lines notelines
current_section = "N"
newnotes = {}
report = {}
for i, line in ipairs(notelines)
newsection = line\match("^%[([^%]]*)%]$")
if newsection
current_section = newsection
continue
timestamp = line\match("^%d+:%d+:%d+") or line\match("^%d+:%d+")
continue if not timestamp
hours, minutes, seconds = line\match("^(%d+):(%d+):(%d+)")
sminutes, sseconds = line\match("^(%d+):(%d+)")
hours or= 0
minutes or= sminutes
seconds or= sseconds
ms = 1000 * (3600 * hours + 60 * minutes + seconds)
newnotes[current_section] or= {}
table.insert(newnotes[current_section], ms)
qc_report = line\match("^[%d:%s%.%-]+(.*)")\gsub("{", "[")\gsub("}", "]")\gsub("\\([^N])", "/%1")
report[ms] or= {}
table.insert(report[ms], qc_report)
for k, v in pairs(newnotes)
table.sort(v)
current_notes = newnotes
notes_owners = {}
allnotes = {}
for k, v in pairs(newnotes)
for i, n in ipairs(v)
notes_owners[n] or= {}
table.insert(notes_owners[n], k)
table.insert(allnotes, n)
table.sort(allnotes)
current_all_notes = allnotes
current_author = nil
clear_markers(subs)
config.c.mark = result.mark
config.c.inline = result.inline
config\write()
sections = fun.table.keys(current_notes)
table.sort(sections)
for i, section in ipairs(sections)
for ni, ms in ipairs(current_notes[section])
si = index_of_closest({i,line.start_time for i, line in ipairs(subs) when line.class == "dialogue"}, ms)
continue if not si
line = subs[si]
if result.inline
for _, note in ipairs(report[ms])
line.text ..= "{|QC|#{note}|}"
line.effect ..= "[QC-#{section}]" if result.mark
subs[si] = line
jump_to = (forward, same, subs, sel) ->
if #current_all_notes == 0
aegisub.log("No notes loaded!\n")
aegisub.cancel()
si = sel[1]
pool = current_all_notes
if same and current_author != nil
pool = current_notes[current_author]
-- we do this dynamically, since lines might have changed of shifted since loading the notes.
subtitle_times = {i,line.start_time for i, line in ipairs(subs) when line.class == "dialogue"}
lines_with_notes_rev = {}
for _, n in ipairs pool
closest_lines_with_notes = index_of_closest(subtitle_times, n)
continue unless closest_lines_with_notes
lines_with_notes_rev[closest_lines_with_notes] = n
lines_with_notes = fun.table.keys lines_with_notes_rev
-- yeeeeeah there are marginally faster algorithms, but that's not necessary at this scale. Let's keep it clean and simple.
table.sort(lines_with_notes)
for i=1,#lines_with_notes
ind = if forward then i else #lines_with_notes + 1 - i
comp = if forward then ((a,b) -> a > b) else ((a, b) -> a < b)
new_si = lines_with_notes[ind]
if comp(new_si, si)
if not same
-- if there are multiple notes on this line, pick one at random. It's very hard to think up a scenario
-- where this would cause problems and not be easily avoidable.
corresponding_note = lines_with_notes_rev[new_si]
if corresponding_note != nil
owners = notes_owners[corresponding_note]
current_author = owners[1] if #owners >= 1
return {new_si}, new_si
mymacros = {}
wrap_register_macro = (name, ...) ->
if haveDepCtrl
table.insert(mymacros, {name, ...})
else
aegisub.register_macro("#{script_name}/#{name}", ...)
wrap_register_macro("Load notes", "Load QC notes", load_notes)
wrap_register_macro("Jump to next note", "Jump to the next note", (...) -> jump_to(true, false, ...))
wrap_register_macro("Jump to previous note", "Jump to the previous note", (...) -> jump_to(false, false, ...))
wrap_register_macro("Jump to next note by author", "Jump to the next note with the same author", (...) -> jump_to(true, true, ...))
wrap_register_macro("Jump to previous note by author", "Jump to the previous note with the same author", (...) -> jump_to(false, true, ...))
wrap_register_macro("Clear all markers", "Clear all the [QC-...] markers that were added when loading the notes", clear_markers)
if haveDepCtrl
depctrl\registerMacros(mymacros)