playrig 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +106 -0
- package/LICENSE +37 -0
- package/README.md +77 -2
- package/bin/playrig.js +4 -0
- package/editions/ae/README.md +144 -0
- package/editions/ae/client/aeb.js +9 -0
- package/editions/ae/examples/hello.jsx +3 -0
- package/editions/ae/examples/inspect.jsx +7 -0
- package/editions/ae/examples/snap.jsx +15 -0
- package/editions/ae/lib/actions.json +159 -0
- package/editions/ae/lib/bridge-api.jsxinc +474 -0
- package/editions/ae/lib/dev.jsxinc +21 -0
- package/editions/ae/lib/icons/README.md +4 -0
- package/editions/ae/lib/icons/dot-error.svg +3 -0
- package/editions/ae/lib/icons/dot-idle.svg +3 -0
- package/editions/ae/lib/icons/dot-running.svg +3 -0
- package/editions/ae/lib/icons/make-png.js +25 -0
- package/editions/ae/lib/icons/png/dot-error.png +0 -0
- package/editions/ae/lib/icons/png/dot-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-running.png +0 -0
- package/editions/ae/lib/icons/png/dot-running@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-off.png +0 -0
- package/editions/ae/lib/icons/png/power-off@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-on.png +0 -0
- package/editions/ae/lib/icons/png/power-on@2x.png +0 -0
- package/editions/ae/lib/icons/png/settings.png +0 -0
- package/editions/ae/lib/icons/png/settings@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-error.png +0 -0
- package/editions/ae/lib/icons/png/status-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-ok.png +0 -0
- package/editions/ae/lib/icons/png/status-ok@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown@2x.png +0 -0
- package/editions/ae/lib/icons/power-off.svg +4 -0
- package/editions/ae/lib/icons/power-on.svg +4 -0
- package/editions/ae/lib/icons/settings.svg +4 -0
- package/editions/ae/lib/icons/status-error.svg +4 -0
- package/editions/ae/lib/icons/status-ok.svg +4 -0
- package/editions/ae/lib/icons/status-rejected.svg +4 -0
- package/editions/ae/lib/icons/status-timeout.svg +4 -0
- package/editions/ae/lib/icons/status-unknown.svg +5 -0
- package/editions/ae/lib/panel-core.jsxinc +249 -0
- package/editions/ae/lib/update-key.pem +11 -0
- package/editions/ae/lib/updater.jsxinc +148 -0
- package/editions/ae/library-starter/INDEX.md +5 -0
- package/editions/ae/library-starter/README.md +161 -0
- package/editions/ae/library-starter/_common.jsx +187 -0
- package/editions/ae/library-starter/categories.json +9 -0
- package/editions/ae/library-starter/recipes/.gitkeep +0 -0
- package/editions/ae/package.json +7 -0
- package/editions/ae/panel/Playrig.jsx +39 -0
- package/editions/ae/panel/core.jsx +1026 -0
- package/editions/ae/skills/create-ae-recipe/SKILL.md +85 -0
- package/editions/ae/skills/create-ae-recipe/references/ae-patterns.md +88 -0
- package/editions/ae/skills/create-ae-recipe/references/video-analysis.md +72 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media +5 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media.py +253 -0
- package/editions/ae/skills/create-ae-recipe/scripts/setup.sh +17 -0
- package/editions/ae/skills/create-ae-video/SKILL.md +88 -0
- package/editions/ae/skills/create-ae-video/references/templates.md +90 -0
- package/editions/ae/skills/create-ae-video/scripts/preflight.js +99 -0
- package/editions/ae/skills/use-ae-recipes/SKILL.md +99 -0
- package/editions/ae/skills/use-ae-recipes/scripts/contact-sheet.jsx +10 -0
- package/editions/ae/skills/use-ae-recipes/scripts/edit-text.jsx +12 -0
- package/editions/ae/skills/use-ae-recipes/scripts/remove-comp.jsx +18 -0
- package/editions/pr/README.md +60 -0
- package/editions/pr/SCORE_PROCESS.md +41 -0
- package/editions/pr/client/aeb.js +9 -0
- package/editions/pr/examples/frame.js +6 -0
- package/editions/pr/examples/hello.js +4 -0
- package/editions/pr/organize.default.json +245 -0
- package/editions/pr/package.json +7 -0
- package/editions/pr/plugin/actions.js +79 -0
- package/editions/pr/plugin/dev.js +19 -0
- package/editions/pr/plugin/icons/README.md +4 -0
- package/editions/pr/plugin/icons/file-code.svg +6 -0
- package/editions/pr/plugin/icons/folder.svg +3 -0
- package/editions/pr/plugin/icons/power-off.svg +4 -0
- package/editions/pr/plugin/icons/power-on.svg +4 -0
- package/editions/pr/plugin/icons/settings.svg +4 -0
- package/editions/pr/plugin/icons/status-error.svg +4 -0
- package/editions/pr/plugin/icons/status-ok.svg +4 -0
- package/editions/pr/plugin/icons/status-rejected.svg +4 -0
- package/editions/pr/plugin/icons/status-timeout.svg +4 -0
- package/editions/pr/plugin/icons/status-unknown.svg +5 -0
- package/editions/pr/plugin/incremental.js +212 -0
- package/editions/pr/plugin/index.html +195 -0
- package/editions/pr/plugin/index.js +710 -0
- package/editions/pr/plugin/manifest.json +45 -0
- package/editions/pr/plugin/organize.js +223 -0
- package/editions/pr/skills/score-video/SKILL.md +69 -0
- package/editions/pr/skills/score-video/scripts/mix.py +112 -0
- package/editions/pr/skills/score-video/scripts/place.js +63 -0
- package/lib/ae.js +754 -0
- package/lib/common.js +132 -0
- package/lib/install.js +148 -0
- package/lib/main.js +107 -0
- package/lib/payload.js +60 -0
- package/lib/pr.js +239 -0
- package/lib/update.js +118 -0
- package/package.json +35 -4
- package/skills/create-video/SKILL.md +72 -0
- package/skills/create-video/references/lessons.md +31 -0
- package/skills/create-video/references/project-conventions.md +46 -0
- package/skills/create-video/scripts/ae/list-project.jsx +8 -0
- package/skills/create-video/scripts/ae/move-chips.jsx +14 -0
- package/skills/create-video/scripts/ae/organize-project.jsx +25 -0
- package/skills/create-video/scripts/ae/quadrant-chips.jsx +143 -0
- package/skills/create-video/scripts/ae/render-comps.jsx +16 -0
- package/skills/create-video/scripts/premiere/export-sequences.js +22 -0
- package/skills/create-video/scripts/premiere/make-sequences.js +24 -0
- package/skills/create-video/scripts/premiere/swap-media.js +43 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
// Playrig (core): runs ExtendScript jobs dropped into <root>/inbox and writes results to <root>/out/<id>/.
|
|
2
|
+
// ES3 only. Loaded by the stub (Playrig.jsx), which owns #targetengine "playrig" and passes in the panel and this folder.
|
|
3
|
+
// The only network use is the updater (lib/updater.jsxinc): it runs curl for the update check and downloads. See README.md.
|
|
4
|
+
|
|
5
|
+
(function (thisObj) {
|
|
6
|
+
var G = $.global;
|
|
7
|
+
var ID_RE = /^[A-Za-z0-9_\-]{1,64}$/;
|
|
8
|
+
var MAX_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
var SETTINGS = "Playrig";
|
|
10
|
+
thisObj = G.__PLAYRIG_PANEL || thisObj;
|
|
11
|
+
var HERE = G.__PLAYRIG_HERE || new File($.fileName).parent.fsName;
|
|
12
|
+
|
|
13
|
+
// State survives panel reloads because of the persistent engine.
|
|
14
|
+
var VERSION = (function () { // from version.json beside this file (written by the payload builder)
|
|
15
|
+
var f = new File(HERE + "/version.json"), t, m;
|
|
16
|
+
try { if (f.exists) { f.encoding = "UTF-8"; f.open("r"); t = f.read(); f.close(); m = /"version"\s*:\s*"([^"]+)"/.exec(t); if (m) { return m[1]; } } } catch (e) { }
|
|
17
|
+
return "dev";
|
|
18
|
+
})();
|
|
19
|
+
var HISTORY_LIMIT = 50;
|
|
20
|
+
var S = G.__PLAYRIG_STATE;
|
|
21
|
+
if (!S) {
|
|
22
|
+
S = G.__PLAYRIG_STATE = { on: false, busy: false, taskId: null, watchId: null, session: [], ui: null, status: "Idle", seq: 0, loaded: false, projPath: null };
|
|
23
|
+
}
|
|
24
|
+
if (!S.session) { S.session = []; }
|
|
25
|
+
S.here = HERE;
|
|
26
|
+
|
|
27
|
+
// ------------------------------------------------------------ settings
|
|
28
|
+
// Settings saved under the panel's former names ("Playpilot", "AE Bridge") are still read, so an upgrade keeps its folder, lib path and options.
|
|
29
|
+
var LEGACY_SETTINGS = ["Playpilot", "AEBridge"];
|
|
30
|
+
function getSet(k, d) {
|
|
31
|
+
var i;
|
|
32
|
+
try { if (app.settings.haveSetting(SETTINGS, k)) { return app.settings.getSetting(SETTINGS, k); } } catch (e) { }
|
|
33
|
+
for (i = 0; i < LEGACY_SETTINGS.length; i++) {
|
|
34
|
+
try { if (app.settings.haveSetting(LEGACY_SETTINGS[i], k)) { return app.settings.getSetting(LEGACY_SETTINGS[i], k); } } catch (e2) { }
|
|
35
|
+
}
|
|
36
|
+
return d;
|
|
37
|
+
}
|
|
38
|
+
function setSet(k, v) { try { app.settings.saveSetting(SETTINGS, k, String(v)); } catch (e) { } }
|
|
39
|
+
|
|
40
|
+
function getRoot() {
|
|
41
|
+
var p = getSet("root", ""), docs, fresh, legacy;
|
|
42
|
+
if (p) { return new Folder(p); }
|
|
43
|
+
// Default <Documents>/Playrig/ae; an existing <Documents>/AE Bridge keeps being used until the new folder exists.
|
|
44
|
+
docs = Folder.myDocuments.fsName;
|
|
45
|
+
fresh = new Folder(docs + "/Playrig/ae");
|
|
46
|
+
legacy = new Folder(docs + "/AE Bridge");
|
|
47
|
+
return (!new Folder(fresh.fsName + "/inbox").exists && new Folder(legacy.fsName + "/inbox").exists) ? legacy : fresh;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sub(name) { return new Folder(getRoot().fsName + "/" + name); }
|
|
51
|
+
|
|
52
|
+
function ensureDirs() {
|
|
53
|
+
var names = ["inbox", "running", "done", "out", "backups", "rejected"], i, f;
|
|
54
|
+
for (i = 0; i < names.length; i++) {
|
|
55
|
+
f = sub(names[i]);
|
|
56
|
+
if (!f.exists && !f.create()) {
|
|
57
|
+
throw new Error("Cannot create " + f.fsName + ". Enable Preferences > Scripting & Expressions > Allow Scripts to Write Files and Access Network.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ------------------------------------------------------------ files
|
|
63
|
+
function readText(f) {
|
|
64
|
+
var s;
|
|
65
|
+
f.encoding = "UTF-8";
|
|
66
|
+
if (!f.open("r")) { throw new Error("Cannot open " + f.fsName); }
|
|
67
|
+
s = f.read();
|
|
68
|
+
f.close();
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
function writeText(f, s) {
|
|
72
|
+
f.encoding = "UTF-8";
|
|
73
|
+
f.lineFeed = "Unix";
|
|
74
|
+
if (!f.open("w")) { throw new Error("Cannot write " + f.fsName); }
|
|
75
|
+
f.write(s);
|
|
76
|
+
f.close();
|
|
77
|
+
}
|
|
78
|
+
function writeAtomic(f, s) {
|
|
79
|
+
var tmp = new File(f.fsName + ".tmp");
|
|
80
|
+
writeText(tmp, s);
|
|
81
|
+
if (f.exists) { f.remove(); }
|
|
82
|
+
if (!tmp.rename(f.name)) { throw new Error("Cannot rename " + tmp.fsName); }
|
|
83
|
+
}
|
|
84
|
+
function moveFile(f, folder) {
|
|
85
|
+
var d = new File(folder.fsName + "/" + f.displayName);
|
|
86
|
+
if (d.exists) { d.remove(); }
|
|
87
|
+
if (!f.copy(d)) { throw new Error("Cannot move " + f.fsName + " to " + folder.fsName); }
|
|
88
|
+
f.remove();
|
|
89
|
+
return d;
|
|
90
|
+
}
|
|
91
|
+
function quote(s) {
|
|
92
|
+
return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n\t]/g, " ").replace(/[^\x20-\x7e]/g, "?") + '"';
|
|
93
|
+
}
|
|
94
|
+
function stamp() {
|
|
95
|
+
var d = new Date();
|
|
96
|
+
function p(n) { return (n < 10 ? "0" : "") + n; }
|
|
97
|
+
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + "-" + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
|
98
|
+
}
|
|
99
|
+
function isoNow(d) {
|
|
100
|
+
var off = -d.getTimezoneOffset(), sign = off < 0 ? "-" : "+";
|
|
101
|
+
function p(n) { return (n < 10 ? "0" : "") + n; }
|
|
102
|
+
off = Math.abs(off);
|
|
103
|
+
return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + "T" + p(d.getHours()) + ":" +
|
|
104
|
+
p(d.getMinutes()) + ":" + p(d.getSeconds()) + sign + p(Math.floor(off / 60)) + ":" + p(off % 60);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Folders that may hold the lib files (bridge-api.jsxinc, panel-core.jsxinc, actions.json, dev.jsxinc, icons/).
|
|
108
|
+
function libDirs() {
|
|
109
|
+
var out = [getSet("lib", ""), HERE, HERE + "/Playrig lib", HERE + "/Playpilot lib", HERE + "/AE Bridge lib", HERE + "/lib", new Folder(HERE).parent.fsName + "/lib", getRoot().fsName + "/lib"], i, r = [], f;
|
|
110
|
+
for (i = 0; i < out.length; i++) {
|
|
111
|
+
if (!out[i]) { continue; }
|
|
112
|
+
f = new File(out[i]);
|
|
113
|
+
// the saved "lib" setting is the path of bridge-api.jsxinc; the others are folders
|
|
114
|
+
r.push(f instanceof File && /\.jsxinc$/i.test(out[i]) ? f.parent.fsName : out[i]);
|
|
115
|
+
}
|
|
116
|
+
return r;
|
|
117
|
+
}
|
|
118
|
+
function findLibFile(name) {
|
|
119
|
+
var d = libDirs(), i, f;
|
|
120
|
+
for (i = 0; i < d.length; i++) {
|
|
121
|
+
f = new File(d[i] + "/" + name);
|
|
122
|
+
if (f.exists) { return f; }
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function findLib() { return findLibFile("bridge-api.jsxinc"); }
|
|
127
|
+
|
|
128
|
+
// Loads the UI-free core (actions, history reader) once. bridge-api is evaluated first: its JSON parser is used for results.
|
|
129
|
+
function ensureCore() {
|
|
130
|
+
var lib, core;
|
|
131
|
+
if (G.PlayrigCore && S.coreOk) { return true; }
|
|
132
|
+
lib = findLib();
|
|
133
|
+
if (!lib) { return false; }
|
|
134
|
+
try { if (!G.PLAYRIG) { $.evalFile(lib); } } catch (e) { return false; }
|
|
135
|
+
core = findLibFile("panel-core.jsxinc");
|
|
136
|
+
if (!core) { return false; }
|
|
137
|
+
try { $.evalFile(core); G.PlayrigCore.init(core.parent.fsName); S.coreOk = true; } catch (e2) { return false; }
|
|
138
|
+
try {
|
|
139
|
+
if (!G.PlayrigDev) { core = findLibFile("dev.jsxinc"); if (core) { $.evalFile(core); } }
|
|
140
|
+
} catch (e3) { }
|
|
141
|
+
try { core = findLibFile("updater.jsxinc"); if (core) { $.evalFile(core); } } catch (e4) { }
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function iconFile(name) {
|
|
146
|
+
var f = findLibFile("icons/png/" + name + ".png");
|
|
147
|
+
return f;
|
|
148
|
+
}
|
|
149
|
+
var imgCache = {};
|
|
150
|
+
function icon(name) {
|
|
151
|
+
var f;
|
|
152
|
+
if (imgCache[name]) { return imgCache[name]; }
|
|
153
|
+
f = iconFile(name);
|
|
154
|
+
try { imgCache[name] = f ? ScriptUI.newImage(f) : null; } catch (e) { imgCache[name] = null; }
|
|
155
|
+
return imgCache[name];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ------------------------------------------------------------ UI helpers
|
|
159
|
+
function ui(fn) {
|
|
160
|
+
// Panel widgets become invalid when the panel is closed; report that as false.
|
|
161
|
+
if (!S.ui) { return false; }
|
|
162
|
+
try { fn(S.ui); return true; } catch (e) { return false; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function colorPen(g, rgb) { return g.newPen(g.PenType.SOLID_COLOR, rgb, 1); }
|
|
166
|
+
|
|
167
|
+
function setStatus(text) {
|
|
168
|
+
var dot;
|
|
169
|
+
S.status = text;
|
|
170
|
+
dot = /^Error/.test(text) ? "dot-error" : ((/^Stopped/.test(text) || !S.on) && !S.busy ? "dot-idle" : "dot-running");
|
|
171
|
+
ui(function (u) {
|
|
172
|
+
u.status.text = text;
|
|
173
|
+
if (icon(dot)) { u.dot.image = icon(dot); }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// One info line under the list. Info fades after 12 s; problems (sticky) stay until replaced.
|
|
178
|
+
function say(text, sticky) {
|
|
179
|
+
var n = (S.msgSeq = (S.msgSeq || 0) + 1);
|
|
180
|
+
S.msgSticky = !!sticky;
|
|
181
|
+
ui(function (u) { u.msg.text = text || ""; });
|
|
182
|
+
if (text && !sticky) { try { app.scheduleTask("Playrig.clearMsg(" + n + ")", 12000, false); } catch (e) { } }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function projectName() {
|
|
186
|
+
var f = null;
|
|
187
|
+
try { f = app.project.file; } catch (e) { }
|
|
188
|
+
return f ? f.displayName.replace(/\.aepx?$/i, "") : "Untitled project (not saved)";
|
|
189
|
+
}
|
|
190
|
+
function projectFile() { try { return app.project.file; } catch (e) { return null; } }
|
|
191
|
+
|
|
192
|
+
// Rows shown in the table: the open project's history from its log, plus this session's jobs of that project that are not in it yet
|
|
193
|
+
// (an unsaved project, a rejected job). Newest first.
|
|
194
|
+
function currentRows() {
|
|
195
|
+
var pf = projectFile(), h = { rows: [], total: 0 }, rows, seen = {}, i, s, path = pf ? pf.fsName : null;
|
|
196
|
+
if (!ensureCore()) { return { rows: [], total: 0, core: false }; }
|
|
197
|
+
if (pf) { try { h = G.PlayrigCore.loadHistory(pf, HISTORY_LIMIT); } catch (e) { h = { rows: [], total: 0 }; } }
|
|
198
|
+
rows = h.rows.slice(0);
|
|
199
|
+
for (i = 0; i < rows.length; i++) { seen[rows[i].id] = true; }
|
|
200
|
+
for (i = 0; i < S.session.length; i++) {
|
|
201
|
+
s = S.session[i];
|
|
202
|
+
if (!seen[s.id] && (s.projectPath || null) === path) { rows.push(s); seen[s.id] = true; }
|
|
203
|
+
}
|
|
204
|
+
rows.sort(function (a, b) { return a.startedAt < b.startedAt ? 1 : (a.startedAt > b.startedAt ? -1 : 0); });
|
|
205
|
+
return { rows: rows.slice(0, HISTORY_LIMIT), total: Math.max(h.total, rows.length), core: true, saved: !!pf };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function refreshHistory() {
|
|
209
|
+
var cur = currentRows();
|
|
210
|
+
S.rows = cur.rows;
|
|
211
|
+
ui(function (u) {
|
|
212
|
+
var i, r, item, h, n = cur.rows.length;
|
|
213
|
+
u.list.removeAll();
|
|
214
|
+
for (i = 0; i < n; i++) {
|
|
215
|
+
r = cur.rows[i];
|
|
216
|
+
// Row icons did not render in AE's multi-column listbox, so the status is a glyph plus the word.
|
|
217
|
+
item = u.list.add("item", G.PlayrigCore.statusGlyph(r.status) + " " + r.status);
|
|
218
|
+
item.subItems[0].text = G.PlayrigCore.actionLabel(r.action, r.actionLabel) + (r.actionSource === "inferred" ? " *" : "");
|
|
219
|
+
item.subItems[1].text = G.PlayrigCore.formatTime(r.startedAt);
|
|
220
|
+
}
|
|
221
|
+
u.count.text = n ? (cur.total > n ? "latest " + n + " of " + cur.total : n + (n === 1 ? " job" : " jobs")) : "";
|
|
222
|
+
// The list is only as tall as its rows (header + rows), never a blank block.
|
|
223
|
+
h = 44 + Math.max(1, Math.min(n, 12)) * 21;
|
|
224
|
+
u.list.preferredSize.height = h;
|
|
225
|
+
u.list.minimumSize.height = h;
|
|
226
|
+
u.summary.text = n ? "Select a job to see what it did. Double-click for details." : (cur.core ? "No jobs yet for this project." : "Panel files missing (lib folder not found). See README, Install.");
|
|
227
|
+
u.details.enabled = false;
|
|
228
|
+
u.win.layout.layout(true);
|
|
229
|
+
});
|
|
230
|
+
if (cur.core && !cur.saved) {
|
|
231
|
+
if (!S.unsavedMsg) { S.unsavedMsg = true; say("Save the project to keep its job history with it.", true); }
|
|
232
|
+
} else if (S.unsavedMsg) { S.unsavedMsg = false; say(""); }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function showProject() {
|
|
236
|
+
var pf = projectFile();
|
|
237
|
+
S.projPath = pf ? pf.fsName : null;
|
|
238
|
+
ui(function (u) { u.project.text = projectName(); });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function sessionPush(r, resultPath) {
|
|
242
|
+
S.session.unshift(G.PlayrigCore.rowFromResult(r, resultPath));
|
|
243
|
+
if (S.session.length > HISTORY_LIMIT) { S.session.length = HISTORY_LIMIT; }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function selectedRow() {
|
|
247
|
+
var out = null;
|
|
248
|
+
ui(function (u) { if (u.list.selection && S.rows) { out = S.rows[u.list.selection.index]; } });
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ------------------------------------------------------------ polling
|
|
253
|
+
function startPolling() {
|
|
254
|
+
if (S.on) { return; }
|
|
255
|
+
try { ensureDirs(); } catch (e) { setStatus("Error"); say("Playrig: " + e.message, true); return; }
|
|
256
|
+
if (!findLib()) {
|
|
257
|
+
var picked = File.openDialog("Locate bridge-api.jsxinc");
|
|
258
|
+
if (!picked) { setStatus("Error"); say("bridge-api.jsxinc not found. See README, Install.", true); return; }
|
|
259
|
+
setSet("lib", picked.fsName);
|
|
260
|
+
}
|
|
261
|
+
ensureCore();
|
|
262
|
+
S.on = true;
|
|
263
|
+
S.taskId = app.scheduleTask("Playrig.tick()", 1000, true);
|
|
264
|
+
setStatus("Idle");
|
|
265
|
+
ui(function (u) { u.power.image = icon("power-on"); u.power.helpTip = "Stop the bridge"; });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function stopPolling() {
|
|
269
|
+
if (S.taskId !== null) { try { app.cancelTask(S.taskId); } catch (e) { } }
|
|
270
|
+
S.taskId = null;
|
|
271
|
+
S.on = false;
|
|
272
|
+
setStatus(S.busy ? S.status : "Stopped");
|
|
273
|
+
ui(function (u) { u.power.image = icon("power-off"); u.power.helpTip = "Start the bridge"; });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ------------------------------------------------------------ rejection
|
|
277
|
+
function simpleResult(id, status, message, extra) {
|
|
278
|
+
var f = new Folder(sub("out").fsName + "/" + id), p = projInfo(), file;
|
|
279
|
+
if (!f.exists) { f.create(); }
|
|
280
|
+
file = new File(f.fsName + "/result.json");
|
|
281
|
+
writeAtomic(file,
|
|
282
|
+
'{\n "id": ' + quote(id) + ',\n "status": ' + quote(status) + ',\n "startedAt": ' + quote(isoNow(new Date())) +
|
|
283
|
+
',\n "project": {"path": ' + (p.path ? quote(p.path) : "null") + ', "dirty": ' + (p.dirty === null ? "null" : String(p.dirty)) + '}' +
|
|
284
|
+
',\n "action": ' + quote(status === "rejected" ? "job.rejected" : "custom") + ',\n "actionSource": "declared"' +
|
|
285
|
+
',\n "error": {"message": ' + quote(message) + '}' + (extra || "") + ',\n "log": [],\n "snapshots": []\n}\n');
|
|
286
|
+
return file;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function reject(f, reason) {
|
|
290
|
+
var base = f.displayName.replace(/\.jsx$/i, ""), id, dest, rf;
|
|
291
|
+
if (ID_RE.test(base)) { id = base; }
|
|
292
|
+
else { S.seq++; id = "rejected-" + stamp() + "-" + S.seq; }
|
|
293
|
+
rf = simpleResult(id, "rejected", reason, ',\n "originalName": ' + quote(f.displayName));
|
|
294
|
+
dest = sub("rejected");
|
|
295
|
+
try {
|
|
296
|
+
if (id !== base) { f.rename(id + ".jsx"); f = new File(f.parent.fsName + "/" + id + ".jsx"); }
|
|
297
|
+
moveFile(f, dest);
|
|
298
|
+
} catch (e) { try { f.remove(); } catch (e2) { } }
|
|
299
|
+
if (ensureCore()) {
|
|
300
|
+
sessionPush({ id: id, status: "rejected", startedAt: isoNow(new Date()), action: "job.rejected", actionSource: "declared", description: null,
|
|
301
|
+
error: { message: reason }, project: projInfo() }, rf.fsName);
|
|
302
|
+
refreshHistory();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ------------------------------------------------------------ job pick
|
|
307
|
+
function pickJob() {
|
|
308
|
+
var files = sub("inbox").getFiles("*.jsx"), valid = [], i, f, base;
|
|
309
|
+
for (i = 0; i < files.length; i++) {
|
|
310
|
+
f = files[i];
|
|
311
|
+
if (!(f instanceof File)) { continue; }
|
|
312
|
+
base = f.displayName.replace(/\.jsx$/i, "");
|
|
313
|
+
if (!ID_RE.test(base)) { reject(f, "File name must match ^[A-Za-z0-9_-]{1,64}$"); continue; }
|
|
314
|
+
if (f.length > MAX_BYTES) { reject(f, "Job file is larger than 2 MB"); continue; }
|
|
315
|
+
valid.push(f);
|
|
316
|
+
}
|
|
317
|
+
if (!valid.length) { return null; }
|
|
318
|
+
valid.sort(function (a, b) { return a.modified.getTime() - b.modified.getTime(); });
|
|
319
|
+
return valid[0];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ------------------------------------------------------------ run
|
|
323
|
+
function errInfo(e, file) {
|
|
324
|
+
var info = { message: e && e.message ? e.message : String(e), line: e && e.line ? Number(e.line) : null,
|
|
325
|
+
fileName: e && e.fileName ? String(e.fileName) : null, source: null }, lines;
|
|
326
|
+
if (file) { info.fileName = file.fsName; }
|
|
327
|
+
try {
|
|
328
|
+
if (file && info.line) {
|
|
329
|
+
lines = readText(file).split(/\r\n|\r|\n/);
|
|
330
|
+
info.source = lines[info.line - 1] !== undefined ? lines[info.line - 1] : null;
|
|
331
|
+
}
|
|
332
|
+
} catch (e2) { }
|
|
333
|
+
return info;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function projInfo() {
|
|
337
|
+
var p = { path: null, dirty: null };
|
|
338
|
+
try { if (app.project.file) { p.path = app.project.file.fsName; } } catch (e) { }
|
|
339
|
+
try { if (app.project.dirty !== undefined) { p.dirty = app.project.dirty; } } catch (e2) { }
|
|
340
|
+
return p;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Copies the project file on disk. app.project.save(File) is "Save As" and would re-point the open project
|
|
344
|
+
// at the backup, so a plain file copy of the last-saved state is used instead.
|
|
345
|
+
function backupProject(id, result) {
|
|
346
|
+
var src = app.project.file, dest;
|
|
347
|
+
if (!src || !src.exists) { result.backup = { ok: false, reason: "project has never been saved" }; return; }
|
|
348
|
+
try {
|
|
349
|
+
dest = new File(sub("backups").fsName + "/" + src.displayName.replace(/\.aepx?$/i, "") + "-" + id + ".aep");
|
|
350
|
+
result.backup = { ok: src.copy(dest), file: dest.fsName, reflectsUnsavedChanges: false };
|
|
351
|
+
} catch (e) { result.backup = { ok: false, reason: e.message }; }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ------------------------------------------------------------ project-folder sync
|
|
355
|
+
// Everything a job produces is mirrored into <project folder>/EDIT_LOGS/ (same folder name as Playrig for Premiere Pro; an older
|
|
356
|
+
// AE-BRIDGE/ folder is renamed to it on the first job) so the project can move to another machine and the next session can
|
|
357
|
+
// read what was done and which assets exist.
|
|
358
|
+
var WS_README = "# EDIT_LOGS\n\n" +
|
|
359
|
+
"Written automatically by Playrig for After Effects next to the .aep. Move this folder together with the project.\n\n" +
|
|
360
|
+
"- `JOURNAL.md`: chronological record of every job (newest last): what ran, status, errors, log, snapshots.\n" +
|
|
361
|
+
"- `jobs/<id>.jsx` (+ `<id>.json` options): the exact scripts that were run on this project.\n" +
|
|
362
|
+
"- `results/<id>/`: `result.json` and the PNG snapshots of each job.\n" +
|
|
363
|
+
"- `assets.ae.md` / `assets.ae.json`: footage, comps and fonts in the project as of the last job (paths relative to the project folder). " +
|
|
364
|
+
"(Named `.ae` so they do not overwrite Playrig for Premiere Pro's `assets.md` when both projects share a folder.)\n\n" +
|
|
365
|
+
"To continue on another system: open the .aep, read `JOURNAL.md` and `assets.ae.md`, re-link any footage marked MISSING, " +
|
|
366
|
+
"install the listed fonts, then submit new jobs with Playrig as usual. Re-running `jobs/*.jsx` in order rebuilds the scripted work.\n";
|
|
367
|
+
|
|
368
|
+
function projectDir() {
|
|
369
|
+
var f = null;
|
|
370
|
+
try { f = app.project.file; } catch (e) { }
|
|
371
|
+
return f ? f.parent : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function copyInto(src, folder) {
|
|
375
|
+
var d;
|
|
376
|
+
if (!folder.exists) { folder.create(); }
|
|
377
|
+
d = new File(folder.fsName + "/" + src.displayName);
|
|
378
|
+
if (d.exists) { d.remove(); }
|
|
379
|
+
return src.copy(d);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function appendText(f, s) {
|
|
383
|
+
f.encoding = "UTF-8";
|
|
384
|
+
f.lineFeed = "Unix";
|
|
385
|
+
if (!f.open("a")) { throw new Error("Cannot append to " + f.fsName); }
|
|
386
|
+
f.write(s);
|
|
387
|
+
f.close();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function assetsMarkdown(a) {
|
|
391
|
+
var s = "# Project assets\n\nGenerated " + a.generatedAt + " by Playrig. Paths are relative to the project folder unless marked external.\n\n", i, x;
|
|
392
|
+
s += "## Footage (" + a.footage.length + ")\n\n";
|
|
393
|
+
for (i = 0; i < a.footage.length; i++) {
|
|
394
|
+
x = a.footage[i];
|
|
395
|
+
s += "- " + x.name + (x.type === "solid" ? " (solid)" : "") + (x.file ? ": `" + x.file + "`" : "") +
|
|
396
|
+
(x.external ? " **EXTERNAL, will not move with the project**" : "") + (x.missing ? " **MISSING**" : "") + "\n";
|
|
397
|
+
}
|
|
398
|
+
s += "\n## Comps (" + a.comps.length + ")\n\n";
|
|
399
|
+
for (i = 0; i < a.comps.length; i++) {
|
|
400
|
+
x = a.comps[i];
|
|
401
|
+
s += "- " + x.name + ": " + x.width + "x" + x.height + ", " + x.fps + " fps, " + x.duration + " s, " + x.numLayers + " layers\n";
|
|
402
|
+
}
|
|
403
|
+
s += "\n## Fonts used by text layers\n\n";
|
|
404
|
+
for (i = 0; i < a.fonts.length; i++) { s += "- " + a.fonts[i] + "\n"; }
|
|
405
|
+
return s;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function journalEntry(result, jobFile) {
|
|
409
|
+
var s = "\n## " + result.id + " [" + result.status + "]\n\n" + (result.description ? "- what: " + result.description + "\n" : "") +
|
|
410
|
+
(result.action ? "- action: " + result.action + (result.actionSource === "inferred" ? " (inferred)" : "") + "\n" : "") + "- started: " + result.startedAt + " (" + result.durationMs + " ms), AE " + result.aeVersion + "\n", i, sn;
|
|
411
|
+
if (jobFile) { s += "- script: `jobs/" + jobFile + "`\n"; }
|
|
412
|
+
if (result.error) { s += "- error: " + result.error.message + (result.error.line ? " (line " + result.error.line + ")" : "") + "\n"; }
|
|
413
|
+
if (result.returnValue !== null && result.returnValue !== undefined) { s += "- return: `" + PLAYRIG.json.stringify(result.returnValue).substr(0, 500) + "`\n"; }
|
|
414
|
+
if (result.log && result.log.length) {
|
|
415
|
+
s += "- log:\n";
|
|
416
|
+
for (i = 0; i < result.log.length && i < 100; i++) { s += " " + String(result.log[i]).replace(/[\r\n]+/g, " ") + "\n"; }
|
|
417
|
+
}
|
|
418
|
+
for (i = 0; result.snapshots && i < result.snapshots.length; i++) {
|
|
419
|
+
sn = result.snapshots[i];
|
|
420
|
+
if (sn.ok) { s += "- snapshot: `results/" + result.id + "/" + sn.file + "` (" + sn.comp + " @ " + sn.time + ")\n"; }
|
|
421
|
+
}
|
|
422
|
+
return s;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function syncOne(ws, id, runFile, optFile, outDir) {
|
|
426
|
+
var res = new Folder(ws.fsName + "/results/" + id), files, i, f;
|
|
427
|
+
if (runFile && runFile.exists) { copyInto(runFile, new Folder(ws.fsName + "/jobs")); }
|
|
428
|
+
if (optFile && optFile.exists) { copyInto(optFile, new Folder(ws.fsName + "/jobs")); }
|
|
429
|
+
if (!res.exists) { res.create(); }
|
|
430
|
+
files = outDir.getFiles();
|
|
431
|
+
for (i = 0; i < files.length; i++) {
|
|
432
|
+
f = files[i];
|
|
433
|
+
if (f instanceof File && !/\.tmp$/i.test(f.name)) { copyInto(f, res); }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Returns {ok, dir} or {ok:false, reason}. Never throws: a sync problem must not fail the job.
|
|
438
|
+
function syncToProject(id, result, runFile, optFile, outDir) {
|
|
439
|
+
var dir = projectDir(), ws, readme, assets, i, pend, p, legacy, migrated;
|
|
440
|
+
if (!dir) {
|
|
441
|
+
S.unsynced = S.unsynced || [];
|
|
442
|
+
S.unsynced.push(id);
|
|
443
|
+
return { ok: false, reason: "project has never been saved, so it has no folder; will sync after the next job once it is saved" };
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
ws = new Folder(dir.fsName + "/" + G.PlayrigCore.logFolder());
|
|
447
|
+
legacy = new Folder(dir.fsName + "/" + G.PlayrigCore.legacyLogFolder());
|
|
448
|
+
migrated = false;
|
|
449
|
+
if (!ws.exists && legacy.exists) {
|
|
450
|
+
// One-time migration of the old AE-BRIDGE/ log folder; if the rename is refused, keep using the old folder.
|
|
451
|
+
if (legacy.rename(ws.name)) { migrated = true; ws = new Folder(dir.fsName + "/" + G.PlayrigCore.logFolder()); }
|
|
452
|
+
else { ws = legacy; }
|
|
453
|
+
}
|
|
454
|
+
if (!ws.exists && !ws.create()) { return { ok: false, reason: "cannot create " + ws.fsName }; }
|
|
455
|
+
readme = new File(ws.fsName + "/README.md");
|
|
456
|
+
if (!readme.exists || migrated) { writeText(readme, WS_README); }
|
|
457
|
+
|
|
458
|
+
// Jobs that ran before the project was first saved.
|
|
459
|
+
pend = S.unsynced || [];
|
|
460
|
+
S.unsynced = [];
|
|
461
|
+
for (i = 0; i < pend.length; i++) {
|
|
462
|
+
try {
|
|
463
|
+
p = new Folder(sub("out").fsName + "/" + pend[i]);
|
|
464
|
+
syncOne(ws, pend[i], new File(sub("done").fsName + "/" + pend[i] + ".jsx"), new File(sub("done").fsName + "/" + pend[i] + ".json"), p);
|
|
465
|
+
appendText(new File(ws.fsName + "/JOURNAL.md"), "\n## " + pend[i] + " (ran before the project was first saved; see results/" + pend[i] + "/result.json)\n");
|
|
466
|
+
} catch (e0) { }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
result.sync = { ok: true, dir: ws.fsName };
|
|
470
|
+
if (migrated) { result.sync.migratedFrom = G.PlayrigCore.legacyLogFolder(); }
|
|
471
|
+
// result.json in the workspace must include the sync block, so rewrite it before copying.
|
|
472
|
+
writeAtomic(new File(outDir.fsName + "/result.json"), PLAYRIG.json.stringify(result, 2) + "\n");
|
|
473
|
+
syncOne(ws, id, runFile, optFile, outDir);
|
|
474
|
+
|
|
475
|
+
if (!new File(ws.fsName + "/JOURNAL.md").exists) { writeText(new File(ws.fsName + "/JOURNAL.md"), "# Playrig journal\n\nProject: " + app.project.file.displayName + "\n"); }
|
|
476
|
+
appendText(new File(ws.fsName + "/JOURNAL.md"), journalEntry(result, runFile ? runFile.displayName : null));
|
|
477
|
+
|
|
478
|
+
assets = PLAYRIG._assets(dir.fsName);
|
|
479
|
+
writeText(new File(ws.fsName + "/assets.ae.json"), PLAYRIG.json.stringify(assets, 2) + "\n");
|
|
480
|
+
writeText(new File(ws.fsName + "/assets.ae.md"), assetsMarkdown(assets));
|
|
481
|
+
if (migrated) { try { new File(ws.fsName + "/assets.json").remove(); new File(ws.fsName + "/assets.md").remove(); } catch (e11) { } } // the old names; this app regenerates them as assets.ae.*
|
|
482
|
+
return result.sync;
|
|
483
|
+
} catch (e) {
|
|
484
|
+
return { ok: false, reason: e.message };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function runJob(inboxFile) {
|
|
489
|
+
var id = inboxFile.displayName.replace(/\.jsx$/i, "");
|
|
490
|
+
var root = getRoot(), t0 = new Date().getTime(), opts = {}, result, runFile, optInbox, optFile, outDir, lib;
|
|
491
|
+
var suppressed = false, undoOpen = false, ready = false, rf;
|
|
492
|
+
var elapsed, list, i, j, o, sn, jobSrc = "", cls;
|
|
493
|
+
|
|
494
|
+
S.busy = true;
|
|
495
|
+
setStatus("Running " + id);
|
|
496
|
+
ensureCore();
|
|
497
|
+
try {
|
|
498
|
+
runFile = moveFile(inboxFile, sub("running"));
|
|
499
|
+
optInbox = new File(sub("inbox").fsName + "/" + id + ".json");
|
|
500
|
+
optFile = optInbox.exists ? moveFile(optInbox, sub("running")) : null;
|
|
501
|
+
outDir = new Folder(sub("out").fsName + "/" + id);
|
|
502
|
+
if (!outDir.exists) { outDir.create(); }
|
|
503
|
+
// Clear a stale result from an earlier job with the same id.
|
|
504
|
+
if (new File(outDir.fsName + "/result.json").exists) { new File(outDir.fsName + "/result.json").remove(); }
|
|
505
|
+
|
|
506
|
+
result = { id: id, status: "ok", startedAt: isoNow(new Date(t0)), durationMs: 0, aeVersion: String(app.version),
|
|
507
|
+
project: projInfo(), action: "custom", actionSource: "inferred", description: null, error: null, log: [], snapshots: [], inspect: null, returnValue: null };
|
|
508
|
+
|
|
509
|
+
G.__PLAYRIG_CTX = { root: root.fsName, outDir: outDir.fsName, id: id };
|
|
510
|
+
try {
|
|
511
|
+
lib = findLib();
|
|
512
|
+
if (!lib) { throw new Error("bridge-api.jsxinc not found"); }
|
|
513
|
+
$.evalFile(lib);
|
|
514
|
+
if (optFile) { opts = PLAYRIG.json.parse(readText(optFile)); }
|
|
515
|
+
jobSrc = readText(runFile);
|
|
516
|
+
if (jobSrc.charCodeAt(0) === 0xFEFF) { jobSrc = jobSrc.substr(1); }
|
|
517
|
+
try { cls = G.PlayrigCore.classify(opts, jobSrc, id); } catch (eC) { cls = { action: "custom", actionSource: "inferred", actionLabel: null, description: null }; }
|
|
518
|
+
result.action = cls.action;
|
|
519
|
+
result.actionSource = cls.actionSource;
|
|
520
|
+
if (cls.actionLabel) { result.actionLabel = cls.actionLabel; }
|
|
521
|
+
result.description = cls.description;
|
|
522
|
+
ready = true;
|
|
523
|
+
} catch (e0) {
|
|
524
|
+
result.status = optFile && G.PLAYRIG ? "rejected" : "error";
|
|
525
|
+
if (result.status === "rejected") { result.action = "job.rejected"; result.actionSource = "declared"; }
|
|
526
|
+
result.error = errInfo(e0, null);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (ready) {
|
|
530
|
+
if (getSet("backup", "1") === "1") { backupProject(id, result); }
|
|
531
|
+
try {
|
|
532
|
+
app.beginSuppressDialogs();
|
|
533
|
+
suppressed = true;
|
|
534
|
+
if (opts.undoGroup !== false) { app.beginUndoGroup("Playrig: " + id); undoOpen = true; }
|
|
535
|
+
// Overriding $.global.alert has no effect on the job, so the job runs inside a function that
|
|
536
|
+
// shadows alert/confirm. The prefix has no newline, so reported line numbers match the file.
|
|
537
|
+
// ExtendScript's eval treats a comment such as "// @action x" as a preprocessor directive and throws a syntax error, so
|
|
538
|
+
// the comment marker is defused in the evaluated copy only (one extra character on the same line, so line numbers still match).
|
|
539
|
+
eval("(function (alert, confirm) {" + jobSrc.replace(/^([ \t]*\/\/)([ \t]*)@/gm, "$1$2~@") + "\n})(" +
|
|
540
|
+
"function (m) { PLAYRIG.log('[alert] ' + m); }, " +
|
|
541
|
+
"function (m) { PLAYRIG.log('[confirm -> false] ' + m); return false; });");
|
|
542
|
+
} catch (e1) {
|
|
543
|
+
result.status = "error";
|
|
544
|
+
result.error = errInfo(e1, runFile);
|
|
545
|
+
} finally {
|
|
546
|
+
if (undoOpen) { try { app.endUndoGroup(); } catch (e2) { } }
|
|
547
|
+
if (suppressed) { try { app.endSuppressDialogs(false); } catch (e3) { } }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
elapsed = new Date().getTime() - t0;
|
|
551
|
+
// ExtendScript cannot be interrupted, so a timeout is only detected after the job returns.
|
|
552
|
+
if (result.status === "ok" && opts.timeoutSec && elapsed > opts.timeoutSec * 1000) {
|
|
553
|
+
result.status = "timeout";
|
|
554
|
+
result.error = { message: "Job ran " + Math.round(elapsed / 1000) + " s, past timeoutSec=" + opts.timeoutSec + " (not interrupted)", line: null, fileName: null, source: null };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Snapshots: requested in options, plus any queued by the job.
|
|
558
|
+
list = [];
|
|
559
|
+
if (opts.snapshots) {
|
|
560
|
+
for (i = 0; i < opts.snapshots.length; i++) {
|
|
561
|
+
sn = opts.snapshots[i];
|
|
562
|
+
for (j = 0; j < (sn.times ? sn.times.length : 0); j++) { list.push({ comp: sn.comp, time: sn.times[j] }); }
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
o = PLAYRIG._snapQueue();
|
|
566
|
+
for (i = 0; i < o.length; i++) { list.push(o[i]); }
|
|
567
|
+
try { result.snapshots = PLAYRIG._takeSnapshots(list, outDir); }
|
|
568
|
+
catch (e4) { result.snapshots = [{ ok: false, error: e4.message }]; }
|
|
569
|
+
|
|
570
|
+
if (opts.inspect && opts.inspect.comp) {
|
|
571
|
+
try { result.inspect = PLAYRIG.inspect(opts.inspect.comp, opts.inspect.depth, opts.inspect.time); }
|
|
572
|
+
catch (e5) { result.inspect = { error: e5.message }; }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
result.log = PLAYRIG._log();
|
|
576
|
+
result.returnValue = PLAYRIG._ret();
|
|
577
|
+
result.project = projInfo();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
result.durationMs = new Date().getTime() - t0;
|
|
581
|
+
try {
|
|
582
|
+
writeAtomic(new File(outDir.fsName + "/result.json"), PLAYRIG.json.stringify(result, 2) + "\n");
|
|
583
|
+
} catch (e6) {
|
|
584
|
+
result.returnValue = "(not serialisable: " + e6.message + ")";
|
|
585
|
+
result.inspect = null;
|
|
586
|
+
writeAtomic(new File(outDir.fsName + "/result.json"), PLAYRIG.json.stringify(result, 2) + "\n");
|
|
587
|
+
}
|
|
588
|
+
// Mirror into the project folder (journal, job script, results, asset manifest) while the job files are
|
|
589
|
+
// still in running/, then rewrite result.json with the outcome so the root copy carries it too.
|
|
590
|
+
if (ready) {
|
|
591
|
+
result.sync = syncToProject(id, result, runFile, optFile, outDir);
|
|
592
|
+
try { writeAtomic(new File(outDir.fsName + "/result.json"), PLAYRIG.json.stringify(result, 2) + "\n"); } catch (e9) { }
|
|
593
|
+
if (result.sync.ok) {
|
|
594
|
+
try { copyInto(new File(outDir.fsName + "/result.json"), new Folder(result.sync.dir + "/results/" + id)); } catch (e10) { }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
try { moveFile(runFile, sub("done")); if (optFile) { moveFile(optFile, sub("done")); } } catch (e7) { }
|
|
598
|
+
sessionPush(result, outDir.fsName + "/result.json");
|
|
599
|
+
refreshHistory();
|
|
600
|
+
setStatus(result.status === "error" ? "Error (" + id + ")" : "Idle");
|
|
601
|
+
} catch (fatal) {
|
|
602
|
+
try {
|
|
603
|
+
rf = simpleResult(id, "error", "Playrig failure: " + fatal.message);
|
|
604
|
+
sessionPush({ id: id, status: "error", startedAt: isoNow(new Date()), action: "custom", actionSource: "declared", error: { message: "Playrig failure: " + fatal.message }, project: projInfo() }, rf.fsName);
|
|
605
|
+
refreshHistory();
|
|
606
|
+
} catch (e8) { }
|
|
607
|
+
setStatus("Error");
|
|
608
|
+
} finally {
|
|
609
|
+
S.busy = false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ------------------------------------------------------------ public engine API
|
|
614
|
+
G.Playrig = {
|
|
615
|
+
tick: function () {
|
|
616
|
+
var job;
|
|
617
|
+
if (!S.on || S.busy) { return; }
|
|
618
|
+
// Panel closed: its widgets are invalid, so stop polling.
|
|
619
|
+
if (!ui(function (u) { var t = u.status.text; })) { stopPolling(); return; }
|
|
620
|
+
try {
|
|
621
|
+
job = pickJob();
|
|
622
|
+
if (job) { runJob(job); }
|
|
623
|
+
} catch (e) {
|
|
624
|
+
S.busy = false;
|
|
625
|
+
setStatus("Error");
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
// Runs even when the bridge is stopped (own task): follows the open project so the table always shows ITS jobs.
|
|
629
|
+
watch: function () {
|
|
630
|
+
var pf, p;
|
|
631
|
+
if (S.busy) { return; }
|
|
632
|
+
if (!ui(function (u) { var t = u.status.text; })) { stopWatching(); return; }
|
|
633
|
+
pf = projectFile();
|
|
634
|
+
p = pf ? pf.fsName : null;
|
|
635
|
+
if (p !== S.projPath) { showProject(); refreshHistory(); }
|
|
636
|
+
},
|
|
637
|
+
clearMsg: function (n) { if (n === S.msgSeq && !S.msgSticky) { ui(function (u) { u.msg.text = ""; }); } },
|
|
638
|
+
start: startPolling,
|
|
639
|
+
stop: stopPolling
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
function startWatching() {
|
|
643
|
+
if (S.watchId !== null && S.watchId !== undefined) { try { app.cancelTask(S.watchId); } catch (e) { } }
|
|
644
|
+
S.watchId = app.scheduleTask("Playrig.watch()", 1500, true);
|
|
645
|
+
}
|
|
646
|
+
function stopWatching() {
|
|
647
|
+
if (S.watchId !== null && S.watchId !== undefined) { try { app.cancelTask(S.watchId); } catch (e) { } }
|
|
648
|
+
S.watchId = null;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ------------------------------------------------------------ links
|
|
652
|
+
function openUrl(url) {
|
|
653
|
+
try {
|
|
654
|
+
if ($.os.indexOf("Windows") >= 0) { system.callSystem('cmd /c start "" "' + url + '"'); }
|
|
655
|
+
else { system.callSystem('open "' + url + '"'); }
|
|
656
|
+
} catch (e) { say("Cannot open " + url, true); }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ------------------------------------------------------------ job details window
|
|
660
|
+
function openDetails(row) {
|
|
661
|
+
var r = null, secs, dev = [], dlg, head, body, i, s, g, lines, et, w = 460, logDir = null, pf = projectFile();
|
|
662
|
+
if (!row || !ensureCore()) { return; }
|
|
663
|
+
try { r = G.PlayrigCore.readResult(row); } catch (e) { r = null; }
|
|
664
|
+
if (!r) {
|
|
665
|
+
// The live row of an unsaved project may only exist in memory.
|
|
666
|
+
r = { id: row.id, status: row.status, startedAt: row.startedAt, durationMs: row.durationMs, action: row.action, actionSource: row.actionSource,
|
|
667
|
+
actionLabel: row.actionLabel, description: row.description, error: row.error ? { message: row.error } : null, log: [], snapshots: [], project: { path: row.projectPath } };
|
|
668
|
+
}
|
|
669
|
+
secs = G.PlayrigCore.detail(r, row);
|
|
670
|
+
if (G.PlayrigDev && G.PlayrigDev.jobDetail) {
|
|
671
|
+
try {
|
|
672
|
+
logDir = G.PlayrigCore.logDir(pf);
|
|
673
|
+
dev = G.PlayrigDev.jobDetail({ logDir: logDir, id: row.id, readText: readText }) || [];
|
|
674
|
+
} catch (e2) { dev = []; }
|
|
675
|
+
}
|
|
676
|
+
secs = secs.concat(dev);
|
|
677
|
+
|
|
678
|
+
dlg = new Window("dialog", "Job details");
|
|
679
|
+
dlg.orientation = "column";
|
|
680
|
+
dlg.alignChildren = ["fill", "top"];
|
|
681
|
+
dlg.margins = 16;
|
|
682
|
+
dlg.spacing = 8;
|
|
683
|
+
head = dlg.add("group");
|
|
684
|
+
head.orientation = "row";
|
|
685
|
+
head.alignChildren = ["left", "center"];
|
|
686
|
+
head.spacing = 8;
|
|
687
|
+
if (icon(G.PlayrigCore.statusIconName(r.status))) { head.add("image", undefined, icon(G.PlayrigCore.statusIconName(r.status))); }
|
|
688
|
+
s = head.add("statictext", undefined, G.PlayrigCore.actionLabel(r.action || row.action, r.actionLabel || row.actionLabel));
|
|
689
|
+
try { s.graphics.font = ScriptUI.newFont("dialog", "BOLD", 13); } catch (e3) { }
|
|
690
|
+
head.add("statictext", undefined, " " + r.status);
|
|
691
|
+
|
|
692
|
+
body = dlg.add("group");
|
|
693
|
+
body.orientation = "column";
|
|
694
|
+
body.alignChildren = ["fill", "top"];
|
|
695
|
+
body.spacing = 6;
|
|
696
|
+
for (i = 0; i < secs.length; i++) {
|
|
697
|
+
g = body.add("group");
|
|
698
|
+
if (secs[i].multi) {
|
|
699
|
+
g.orientation = "column";
|
|
700
|
+
g.alignChildren = ["fill", "top"];
|
|
701
|
+
g.spacing = 2;
|
|
702
|
+
g.add("statictext", undefined, secs[i].title);
|
|
703
|
+
lines = String(secs[i].text).split(/\r\n|\r|\n/).length;
|
|
704
|
+
et = g.add("edittext", undefined, String(secs[i].text), { multiline: true, readonly: true, scrollable: true });
|
|
705
|
+
et.preferredSize = [w, Math.min(Math.max(lines, 1), 10) * 15 + 8];
|
|
706
|
+
} else {
|
|
707
|
+
g.orientation = "row";
|
|
708
|
+
g.alignChildren = ["left", "top"];
|
|
709
|
+
g.spacing = 8;
|
|
710
|
+
s = g.add("statictext", undefined, secs[i].title);
|
|
711
|
+
s.preferredSize.width = 96;
|
|
712
|
+
try { s.graphics.foregroundColor = colorPen(s.graphics, [0.55, 0.55, 0.55, 1]); } catch (e4) { }
|
|
713
|
+
s = g.add("statictext", undefined, String(secs[i].text), { multiline: true });
|
|
714
|
+
s.preferredSize.width = w - 104;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
g = dlg.add("group");
|
|
718
|
+
g.alignment = ["right", "top"];
|
|
719
|
+
g.margins = [0, 8, 0, 0];
|
|
720
|
+
g.add("button", undefined, "Done", { name: "ok" });
|
|
721
|
+
dlg.show();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ------------------------------------------------------------ updates
|
|
725
|
+
// Check: once a day while "Check for updates automatically" is on (default), or when the user presses the button.
|
|
726
|
+
// Install: only on consent (the banner / About button), unless "Install updates automatically" is on (default off).
|
|
727
|
+
var updCfg = false;
|
|
728
|
+
function updater() {
|
|
729
|
+
if (!ensureCore() || !G.PlayrigUpdater) { return null; }
|
|
730
|
+
if (!updCfg) {
|
|
731
|
+
G.PlayrigUpdater.configure({ here: HERE, getSet: getSet, setSet: setSet, libFile: findLibFile, parse: function (t) { return G.PLAYRIG.json.parse(t); } });
|
|
732
|
+
updCfg = true;
|
|
733
|
+
}
|
|
734
|
+
return G.PlayrigUpdater;
|
|
735
|
+
}
|
|
736
|
+
function showBanner(text, canInstall) {
|
|
737
|
+
S.updText = text;
|
|
738
|
+
S.updCanInstall = !!canInstall;
|
|
739
|
+
if (S.ui && S.ui.updRow) {
|
|
740
|
+
S.ui.updText.text = text || "";
|
|
741
|
+
S.ui.updBtn.visible = !!canInstall;
|
|
742
|
+
S.ui.updRow.visible = !!text;
|
|
743
|
+
try { S.ui.win.layout.layout(true); } catch (e) { }
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Returns { ok, text, info, canInstall }. Never throws.
|
|
747
|
+
function runUpdateCheck() {
|
|
748
|
+
var U = updater(), r, can;
|
|
749
|
+
if (!U) { return { ok: false, text: "The updater is not installed with this panel." }; }
|
|
750
|
+
try { r = U.check(15); } catch (e) { return { ok: false, text: "Update check failed: " + e.message }; }
|
|
751
|
+
if (!r.ok) { return { ok: false, text: r.error }; }
|
|
752
|
+
S.updInfo = r;
|
|
753
|
+
if (!r.newer) { return { ok: true, text: r.error || ("You are up to date (version " + r.current + ")."), info: r }; }
|
|
754
|
+
can = U.canVerify();
|
|
755
|
+
return { ok: true, info: r, canInstall: can, text: "Version " + r.latest + " is available (you have " + r.current + ")." + (r.notes ? " " + r.notes : "") + (can ? "" : " Run `playrig update ae` in a terminal to install it.") };
|
|
756
|
+
}
|
|
757
|
+
function runInstall() {
|
|
758
|
+
var U = updater(), r;
|
|
759
|
+
if (!U || !S.updInfo || !S.updInfo.entry) { return { ok: false, text: "Check for updates first." }; }
|
|
760
|
+
r = U.install(S.updInfo.entry);
|
|
761
|
+
if (!r.ok) { return { ok: false, text: r.error }; }
|
|
762
|
+
S.updInfo = null;
|
|
763
|
+
showBanner("Updated to " + r.version + ". Restart After Effects to use it.", false);
|
|
764
|
+
return { ok: true, text: "Updated to " + r.version + ". Restart After Effects to use it." };
|
|
765
|
+
}
|
|
766
|
+
function autoUpdateCheck() {
|
|
767
|
+
var U, r;
|
|
768
|
+
if (getSet("updateAutoCheck", "1") !== "1") { return; }
|
|
769
|
+
U = updater();
|
|
770
|
+
if (!U || !U.due()) { return; }
|
|
771
|
+
r = runUpdateCheck();
|
|
772
|
+
if (!r.ok || !r.info || !r.info.newer) { return; }
|
|
773
|
+
if (getSet("updateAutoInstall", "0") === "1" && r.canInstall) { r = runInstall(); showBanner(r.text, false); return; }
|
|
774
|
+
showBanner(r.text, r.canInstall);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ------------------------------------------------------------ settings window
|
|
778
|
+
function isAbsPath(p) { return /^(\/|[A-Za-z]:[\\\/]|\\\\)/.test(p); }
|
|
779
|
+
|
|
780
|
+
function openSettings() {
|
|
781
|
+
var dlg = new Window("dialog", "Playrig Settings"), nav, stack, panes = [], p, g, row, rootEdit, browse, hint, cbBackup, cbAuto, msg, i, t, done, prev, updBtn, updInstall, updMsg, cbUpdCheck, cbUpdAuto;
|
|
782
|
+
dlg.orientation = "column";
|
|
783
|
+
dlg.alignChildren = ["fill", "top"];
|
|
784
|
+
dlg.margins = 16;
|
|
785
|
+
dlg.spacing = 12;
|
|
786
|
+
|
|
787
|
+
row = dlg.add("group");
|
|
788
|
+
row.orientation = "row";
|
|
789
|
+
row.alignChildren = ["fill", "top"];
|
|
790
|
+
row.spacing = 16;
|
|
791
|
+
nav = row.add("listbox", undefined, ["General", "About"]);
|
|
792
|
+
nav.preferredSize = [110, 70];
|
|
793
|
+
nav.alignment = ["left", "top"];
|
|
794
|
+
stack = row.add("group");
|
|
795
|
+
stack.orientation = "stack";
|
|
796
|
+
stack.alignment = ["fill", "top"];
|
|
797
|
+
stack.alignChildren = ["fill", "top"];
|
|
798
|
+
|
|
799
|
+
// General
|
|
800
|
+
p = stack.add("group");
|
|
801
|
+
p.orientation = "column";
|
|
802
|
+
p.alignChildren = ["fill", "top"];
|
|
803
|
+
p.spacing = 6;
|
|
804
|
+
p.add("statictext", undefined, "Playrig folder");
|
|
805
|
+
g = p.add("group");
|
|
806
|
+
g.orientation = "row";
|
|
807
|
+
g.alignChildren = ["fill", "center"];
|
|
808
|
+
g.spacing = 6;
|
|
809
|
+
rootEdit = g.add("edittext", undefined, getRoot().fsName);
|
|
810
|
+
rootEdit.alignment = ["fill", "center"];
|
|
811
|
+
rootEdit.preferredSize.width = 270;
|
|
812
|
+
browse = g.add("button", undefined, "Browse...");
|
|
813
|
+
browse.alignment = ["right", "center"];
|
|
814
|
+
hint = p.add("statictext", undefined, "Where jobs and results are exchanged: inbox, out, done, backups. Not your project folder. Keep it private: anything that can write to inbox can run code in the app.", { multiline: true });
|
|
815
|
+
hint.preferredSize = [380, 48];
|
|
816
|
+
try { hint.graphics.foregroundColor = colorPen(hint.graphics, [0.55, 0.55, 0.55, 1]); } catch (e0) { }
|
|
817
|
+
cbAuto = p.add("checkbox", undefined, "Start automatically");
|
|
818
|
+
cbBackup = p.add("checkbox", undefined, "Back up the project file before each job");
|
|
819
|
+
cbAuto.value = getSet("auto", "0") === "1";
|
|
820
|
+
cbBackup.value = getSet("backup", "1") === "1";
|
|
821
|
+
panes.push(p);
|
|
822
|
+
|
|
823
|
+
// About
|
|
824
|
+
p = stack.add("group");
|
|
825
|
+
p.orientation = "column";
|
|
826
|
+
p.alignChildren = ["left", "top"];
|
|
827
|
+
p.spacing = 6;
|
|
828
|
+
t = p.add("statictext", undefined, "Playrig");
|
|
829
|
+
try { t.graphics.font = ScriptUI.newFont("dialog", "BOLD", 13); } catch (e1) { }
|
|
830
|
+
p.add("statictext", undefined, "Version " + VERSION);
|
|
831
|
+
p.add("statictext", undefined, "Developed by Vivek Nigam");
|
|
832
|
+
g = p.add("group");
|
|
833
|
+
g.orientation = "row";
|
|
834
|
+
g.spacing = 6;
|
|
835
|
+
g.margins = [0, 8, 0, 0];
|
|
836
|
+
g.add("statictext", undefined, "Twitter");
|
|
837
|
+
g.add("button", undefined, "@viveknigam_").onClick = function () { openUrl("https://twitter.com/viveknigam_"); };
|
|
838
|
+
g = p.add("group");
|
|
839
|
+
g.orientation = "row";
|
|
840
|
+
g.spacing = 6;
|
|
841
|
+
g.add("statictext", undefined, "Instagram");
|
|
842
|
+
g.add("button", undefined, "@viveknigam3003").onClick = function () { openUrl("https://www.instagram.com/viveknigam3003/"); };
|
|
843
|
+
g = p.add("group");
|
|
844
|
+
g.orientation = "row";
|
|
845
|
+
g.spacing = 6;
|
|
846
|
+
g.margins = [0, 8, 0, 0];
|
|
847
|
+
updBtn = g.add("button", undefined, "Check for Updates");
|
|
848
|
+
updInstall = g.add("button", undefined, "Install update");
|
|
849
|
+
updInstall.visible = false;
|
|
850
|
+
updMsg = p.add("statictext", undefined, S.updText || "", { multiline: true });
|
|
851
|
+
updMsg.preferredSize = [330, 44];
|
|
852
|
+
cbUpdCheck = p.add("checkbox", undefined, "Check for updates automatically (once a day)");
|
|
853
|
+
cbUpdAuto = p.add("checkbox", undefined, "Install updates automatically");
|
|
854
|
+
cbUpdCheck.value = getSet("updateAutoCheck", "1") === "1";
|
|
855
|
+
cbUpdAuto.value = getSet("updateAutoInstall", "0") === "1";
|
|
856
|
+
t = p.add("statictext", undefined, "© 2026 Vivek Nigam. All rights reserved.");
|
|
857
|
+
t = p.add("statictext", undefined, "Icons by Tabler Icons (MIT License)");
|
|
858
|
+
panes.push(p);
|
|
859
|
+
|
|
860
|
+
function showPane(n) { var k; for (k = 0; k < panes.length; k++) { panes[k].visible = (k === n); } }
|
|
861
|
+
nav.onChange = function () { if (nav.selection) { showPane(nav.selection.index); } };
|
|
862
|
+
nav.selection = 0;
|
|
863
|
+
showPane(0);
|
|
864
|
+
|
|
865
|
+
// The modal gets its own message line: messages in the panel are not visible behind it.
|
|
866
|
+
msg = dlg.add("statictext", undefined, "", { truncate: "end" });
|
|
867
|
+
try { msg.graphics.foregroundColor = colorPen(msg.graphics, [0.9, 0.75, 0.2, 1]); } catch (e2) { }
|
|
868
|
+
|
|
869
|
+
function applyRoot(text) {
|
|
870
|
+
var v = text.replace(/^\s+|\s+$/g, ""), f, old = getRoot().fsName;
|
|
871
|
+
if (v === old) { msg.text = ""; return true; }
|
|
872
|
+
if (!isAbsPath(v)) { msg.text = "The bridge folder must be an absolute path."; rootEdit.text = old; return false; }
|
|
873
|
+
f = new Folder(v);
|
|
874
|
+
try {
|
|
875
|
+
if (!f.exists && !f.create()) { throw new Error("Cannot create " + f.fsName); }
|
|
876
|
+
} catch (e) { msg.text = "Cannot use that folder: " + e.message; rootEdit.text = old; return false; }
|
|
877
|
+
setSet("root", f.fsName);
|
|
878
|
+
rootEdit.text = f.fsName;
|
|
879
|
+
try { ensureDirs(); msg.text = ""; } catch (e5) { msg.text = e5.message; setSet("root", old); rootEdit.text = old; return false; }
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
rootEdit.onChange = function () { applyRoot(rootEdit.text); };
|
|
883
|
+
browse.onClick = function () {
|
|
884
|
+
var f = Folder.selectDialog("Choose the Playrig folder", getRoot());
|
|
885
|
+
if (f) { rootEdit.text = f.fsName; applyRoot(f.fsName); }
|
|
886
|
+
};
|
|
887
|
+
cbAuto.onClick = function () { setSet("auto", cbAuto.value ? "1" : "0"); };
|
|
888
|
+
cbBackup.onClick = function () { setSet("backup", cbBackup.value ? "1" : "0"); };
|
|
889
|
+
cbUpdCheck.onClick = function () { setSet("updateAutoCheck", cbUpdCheck.value ? "1" : "0"); };
|
|
890
|
+
cbUpdAuto.onClick = function () { setSet("updateAutoInstall", cbUpdAuto.value ? "1" : "0"); };
|
|
891
|
+
updBtn.onClick = function () {
|
|
892
|
+
var r;
|
|
893
|
+
updBtn.enabled = false; updMsg.text = "Checking..."; try { dlg.update(); } catch (eU) { }
|
|
894
|
+
r = runUpdateCheck();
|
|
895
|
+
updMsg.text = r.text || "";
|
|
896
|
+
updInstall.visible = !!(r.ok && r.canInstall);
|
|
897
|
+
if (r.ok && r.info && r.info.newer) { showBanner(r.text, r.canInstall); }
|
|
898
|
+
updBtn.enabled = true;
|
|
899
|
+
};
|
|
900
|
+
updInstall.onClick = function () {
|
|
901
|
+
var r;
|
|
902
|
+
updInstall.enabled = false; updMsg.text = "Downloading and verifying..."; try { dlg.update(); } catch (eU2) { }
|
|
903
|
+
r = runInstall();
|
|
904
|
+
updMsg.text = r.text;
|
|
905
|
+
updInstall.visible = false; updInstall.enabled = true;
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
g = dlg.add("group");
|
|
909
|
+
g.alignment = ["right", "top"];
|
|
910
|
+
done = g.add("button", undefined, "Done", { name: "ok" });
|
|
911
|
+
dlg.onClose = function () { if (rootEdit.text !== getRoot().fsName) { applyRoot(rootEdit.text); } return true; };
|
|
912
|
+
dlg.show();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ------------------------------------------------------------ UI
|
|
916
|
+
var win = (thisObj instanceof Panel) ? thisObj : new Window("palette", "Playrig", undefined, { resizeable: true });
|
|
917
|
+
win.orientation = "column";
|
|
918
|
+
win.alignChildren = ["fill", "top"];
|
|
919
|
+
win.spacing = 6;
|
|
920
|
+
win.margins = 10;
|
|
921
|
+
|
|
922
|
+
// Header: title + project name (4 px apart) on the left; status dot, power and settings on the right.
|
|
923
|
+
var header = win.add("group");
|
|
924
|
+
header.orientation = "row";
|
|
925
|
+
header.alignChildren = ["fill", "top"];
|
|
926
|
+
header.spacing = 8;
|
|
927
|
+
var titleCol = header.add("group");
|
|
928
|
+
titleCol.orientation = "column";
|
|
929
|
+
titleCol.alignChildren = ["left", "top"];
|
|
930
|
+
titleCol.alignment = ["fill", "top"];
|
|
931
|
+
titleCol.spacing = 4;
|
|
932
|
+
var title = titleCol.add("statictext", undefined, "Playrig");
|
|
933
|
+
try { title.graphics.font = ScriptUI.newFont("dialog", "BOLD", 14); } catch (eT) { }
|
|
934
|
+
var projectText = titleCol.add("statictext", undefined, projectName(), { truncate: "middle" });
|
|
935
|
+
projectText.alignment = ["fill", "top"];
|
|
936
|
+
try { projectText.graphics.foregroundColor = colorPen(projectText.graphics, [0.55, 0.55, 0.55, 1]); } catch (eP) { }
|
|
937
|
+
|
|
938
|
+
var ctl = header.add("group");
|
|
939
|
+
ctl.orientation = "row";
|
|
940
|
+
ctl.alignChildren = ["right", "center"];
|
|
941
|
+
ctl.alignment = ["right", "top"];
|
|
942
|
+
ctl.spacing = 6;
|
|
943
|
+
var dotImg = ctl.add("image", undefined, icon("dot-idle"));
|
|
944
|
+
var status = ctl.add("statictext", undefined, "Idle", { truncate: "end" });
|
|
945
|
+
status.preferredSize.width = 60;
|
|
946
|
+
var power = ctl.add("iconbutton", undefined, icon(S.on ? "power-on" : "power-off"), { style: "toolbutton" });
|
|
947
|
+
power.preferredSize = [24, 24];
|
|
948
|
+
power.helpTip = S.on ? "Stop the bridge" : "Start the bridge";
|
|
949
|
+
var gear = ctl.add("iconbutton", undefined, icon("settings"), { style: "toolbutton" });
|
|
950
|
+
gear.preferredSize = [24, 24];
|
|
951
|
+
gear.helpTip = "Playrig Settings";
|
|
952
|
+
|
|
953
|
+
var listHead = win.add("group");
|
|
954
|
+
listHead.orientation = "row";
|
|
955
|
+
listHead.alignChildren = ["fill", "center"];
|
|
956
|
+
listHead.margins = [0, 10, 0, 0]; // + the window spacing = a 16 px gap under the project name
|
|
957
|
+
var listTitle = listHead.add("statictext", undefined, "Recent jobs");
|
|
958
|
+
listTitle.alignment = ["left", "center"];
|
|
959
|
+
var countText = listHead.add("statictext", undefined, "", { truncate: "end" });
|
|
960
|
+
countText.alignment = ["fill", "center"];
|
|
961
|
+
countText.justify = "right";
|
|
962
|
+
try { countText.graphics.foregroundColor = colorPen(countText.graphics, [0.55, 0.55, 0.55, 1]); } catch (eC) { }
|
|
963
|
+
|
|
964
|
+
var list = win.add("listbox", undefined, [], { numberOfColumns: 3, showHeaders: true, columnTitles: ["Status", "Action", "Time"], columnWidths: [74, 128, 98], multiselect: false });
|
|
965
|
+
list.preferredSize = [300, 47];
|
|
966
|
+
list.alignment = ["fill", "top"];
|
|
967
|
+
|
|
968
|
+
var sumRow = win.add("group");
|
|
969
|
+
sumRow.orientation = "row";
|
|
970
|
+
sumRow.alignChildren = ["fill", "top"];
|
|
971
|
+
sumRow.spacing = 6;
|
|
972
|
+
var summary = sumRow.add("statictext", undefined, "", { multiline: true });
|
|
973
|
+
summary.alignment = ["fill", "top"];
|
|
974
|
+
summary.preferredSize = [210, 44];
|
|
975
|
+
var detailsBtn = sumRow.add("button", undefined, "Details...");
|
|
976
|
+
detailsBtn.alignment = ["right", "top"];
|
|
977
|
+
detailsBtn.enabled = false;
|
|
978
|
+
|
|
979
|
+
var updRow = win.add("group");
|
|
980
|
+
updRow.orientation = "row";
|
|
981
|
+
updRow.alignChildren = ["fill", "center"];
|
|
982
|
+
updRow.spacing = 6;
|
|
983
|
+
var updText = updRow.add("statictext", undefined, "", { truncate: "end" });
|
|
984
|
+
updText.alignment = ["fill", "center"];
|
|
985
|
+
var updBtn = updRow.add("button", undefined, "Install");
|
|
986
|
+
updBtn.alignment = ["right", "center"];
|
|
987
|
+
updBtn.visible = false;
|
|
988
|
+
updRow.visible = false;
|
|
989
|
+
updBtn.onClick = function () { var r = runInstall(); showBanner(r.text, false); };
|
|
990
|
+
|
|
991
|
+
var msgText = win.add("statictext", undefined, "", { truncate: "end" });
|
|
992
|
+
msgText.alignment = ["fill", "top"];
|
|
993
|
+
try { msgText.graphics.foregroundColor = colorPen(msgText.graphics, [0.9, 0.75, 0.2, 1]); } catch (eM) { }
|
|
994
|
+
|
|
995
|
+
S.ui = { updRow: updRow, updText: updText, updBtn: updBtn, win: win, status: status, dot: dotImg, power: power, list: list, project: projectText, count: countText, summary: summary, details: detailsBtn, msg: msgText };
|
|
996
|
+
status.text = S.status;
|
|
997
|
+
|
|
998
|
+
power.onClick = function () { if (S.on) { stopPolling(); } else { startPolling(); } };
|
|
999
|
+
gear.onClick = function () { openSettings(); };
|
|
1000
|
+
list.onChange = function () {
|
|
1001
|
+
var r = selectedRow();
|
|
1002
|
+
summary.text = r ? G.PlayrigCore.summaryText(r) : "Select a job to see what it did. Double-click for details.";
|
|
1003
|
+
detailsBtn.enabled = !!r;
|
|
1004
|
+
};
|
|
1005
|
+
list.onDoubleClick = function () { var r = selectedRow(); if (r) { openDetails(r); } };
|
|
1006
|
+
detailsBtn.onClick = function () { var r = selectedRow(); if (r) { openDetails(r); } };
|
|
1007
|
+
|
|
1008
|
+
win.onResizing = win.onResize = function () { this.layout.resize(); };
|
|
1009
|
+
win.onClose = function () { stopPolling(); stopWatching(); S.ui = null; };
|
|
1010
|
+
|
|
1011
|
+
if (win instanceof Window) { win.center(); win.show(); } else { win.layout.layout(true); }
|
|
1012
|
+
|
|
1013
|
+
S.unsavedMsg = false;
|
|
1014
|
+
ensureCore();
|
|
1015
|
+
showProject();
|
|
1016
|
+
refreshHistory();
|
|
1017
|
+
setStatus(S.status);
|
|
1018
|
+
startWatching();
|
|
1019
|
+
if (!S.loaded) {
|
|
1020
|
+
S.loaded = true;
|
|
1021
|
+
if (getSet("auto", "0") === "1") { startPolling(); }
|
|
1022
|
+
try { autoUpdateCheck(); } catch (eAU) { }
|
|
1023
|
+
} else if (S.updText) {
|
|
1024
|
+
showBanner(S.updText, S.updCanInstall);
|
|
1025
|
+
}
|
|
1026
|
+
})(this);
|