@kubohiroya/turbowarp-plus-editor-git 0.8.0 → 0.9.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/dist/index.js +350 -13
- package/dist/project-assets.d.ts +4 -0
- package/dist/project-change-list.d.ts +81 -0
- package/dist/project-change-model.d.ts +73 -0
- package/dist/project-source-diff.d.ts +2 -0
- package/dist/project-source-diff.js +2 -2
- package/dist/project-source-review.d.ts +6 -1
- package/dist/{repository-BQkADJmK.js → repository-VBp-UNWc.js} +67 -4
- package/dist/repository.d.ts +24 -0
- package/dist/repository.js +1 -1
- package/dist/{scratch-block-diff-renderer-xS8OYsRl.js → scratch-block-diff-renderer-DZR8ep-T.js} +264 -12
- package/dist/style.css +17 -0
- package/package.json +1 -1
- package/src/index.tsx +38 -11
- package/src/project-assets.ts +19 -7
- package/src/project-change-list.tsx +210 -0
- package/src/project-change-model.ts +212 -0
- package/src/project-source-diff.tsx +3 -0
- package/src/project-source-review.tsx +12 -5
- package/src/repository.ts +53 -4
- package/src/style.css +17 -0
|
@@ -16795,6 +16795,7 @@ async function runRemoteOperation(root, operation, getSession, guard, signal) {
|
|
|
16795
16795
|
}
|
|
16796
16796
|
// src/repository.ts
|
|
16797
16797
|
init_objectSpread2();
|
|
16798
|
+
var projectSourceLimit = 16777216;
|
|
16798
16799
|
var emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
16799
16800
|
var textLimit = 1048576;
|
|
16800
16801
|
var inScope = (path, scope) => !scope || path === scope || path.startsWith(scope + "/");
|
|
@@ -17170,13 +17171,31 @@ async function openRepository(root) {
|
|
|
17170
17171
|
}));
|
|
17171
17172
|
return blob.byteLength <= limit ? blob : null;
|
|
17172
17173
|
};
|
|
17173
|
-
const
|
|
17174
|
+
const comparisonSource = async (snapshot, comparison, version) => {
|
|
17175
|
+
if (comparison.kind === "worktree") return version === "after" ? { kind: "worktree" } : snapshot.head ? {
|
|
17176
|
+
kind: "tree",
|
|
17177
|
+
ref: snapshot.head
|
|
17178
|
+
} : null;
|
|
17179
|
+
if (version === "after") return {
|
|
17180
|
+
kind: "tree",
|
|
17181
|
+
ref: comparison.commit
|
|
17182
|
+
};
|
|
17183
|
+
const parent = (await readCommit(_objectSpread2(_objectSpread2({}, options), {}, {
|
|
17184
|
+
cache,
|
|
17185
|
+
oid: comparison.commit
|
|
17186
|
+
}))).commit.parent[0];
|
|
17187
|
+
return parent ? {
|
|
17188
|
+
kind: "tree",
|
|
17189
|
+
ref: parent
|
|
17190
|
+
} : null;
|
|
17191
|
+
};
|
|
17192
|
+
const readAsset = (snapshot, change, version, md5ext, signal) => readFolderAsset(snapshot, pathParts(change.path).slice(0, -1), () => sourceOf(snapshot, change, version), md5ext, signal);
|
|
17193
|
+
const readFolderAsset = async (snapshot, folder, resolve, md5ext, signal) => {
|
|
17174
17194
|
if (signal.aborted) return null;
|
|
17175
17195
|
fs.clearReads();
|
|
17176
17196
|
await assertSnapshot(snapshot);
|
|
17177
|
-
const source = await
|
|
17197
|
+
const source = await resolve();
|
|
17178
17198
|
if (!source) return null;
|
|
17179
|
-
const folder = pathParts(change.path).slice(0, -1);
|
|
17180
17199
|
const settings = await readVersion(source, [...folder, "sb3-source.json"].join("/"), folderSettingsLimit);
|
|
17181
17200
|
const directory = assetsDirectoryParts(settings && new TextDecoder().decode(settings));
|
|
17182
17201
|
if (!directory || signal.aborted) return null;
|
|
@@ -17188,6 +17207,43 @@ async function openRepository(root) {
|
|
|
17188
17207
|
await fs.checkReads();
|
|
17189
17208
|
return bytes && !signal.aborted ? new Blob([bytes]) : null;
|
|
17190
17209
|
};
|
|
17210
|
+
/** Reads the project file and the assets directory of both versions of a project folder. */
|
|
17211
|
+
const readProject = async (snapshot, folder, comparison) => {
|
|
17212
|
+
fs.clearReads();
|
|
17213
|
+
await assertSnapshot(snapshot);
|
|
17214
|
+
const parts = pathParts(folder);
|
|
17215
|
+
const read = async (version) => {
|
|
17216
|
+
const source = await comparisonSource(snapshot, comparison, version);
|
|
17217
|
+
if (!source) return {
|
|
17218
|
+
source: null,
|
|
17219
|
+
assetsDirectory: "assets"
|
|
17220
|
+
};
|
|
17221
|
+
const bytes = await readVersion(source, [...parts, "project.source.json"].join("/"), projectSourceLimit);
|
|
17222
|
+
const settings = await readVersion(source, [...parts, "sb3-source.json"].join("/"), folderSettingsLimit);
|
|
17223
|
+
const directory = assetsDirectoryParts(settings && new TextDecoder().decode(settings));
|
|
17224
|
+
let text = null;
|
|
17225
|
+
if (bytes) try {
|
|
17226
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
17227
|
+
} catch (_unused4) {
|
|
17228
|
+
throw new GitPanelError("unsupported");
|
|
17229
|
+
}
|
|
17230
|
+
return {
|
|
17231
|
+
source: text,
|
|
17232
|
+
assetsDirectory: directory ? directory.join("/") : "assets"
|
|
17233
|
+
};
|
|
17234
|
+
};
|
|
17235
|
+
const before = await read("before"), after = await read("after");
|
|
17236
|
+
await assertSnapshot(snapshot);
|
|
17237
|
+
await fs.checkReads();
|
|
17238
|
+
return {
|
|
17239
|
+
before: before.source,
|
|
17240
|
+
after: after.source,
|
|
17241
|
+
assetsDirectory: {
|
|
17242
|
+
before: before.assetsDirectory,
|
|
17243
|
+
after: after.assetsDirectory
|
|
17244
|
+
}
|
|
17245
|
+
};
|
|
17246
|
+
};
|
|
17191
17247
|
const directoryAt = async (path) => {
|
|
17192
17248
|
let directory = root;
|
|
17193
17249
|
for (const part of pathParts(path)) directory = await directory.getDirectoryHandle(part);
|
|
@@ -17274,7 +17330,14 @@ async function openRepository(root) {
|
|
|
17274
17330
|
* commit changes the first parent with the commit. Each version's sb3-source.json names its assets directory.
|
|
17275
17331
|
* Reads never write, and resolve `null` for anything missing, invalid, larger than 10 MiB or changed since the snapshot.
|
|
17276
17332
|
*/
|
|
17277
|
-
assetLoader: (snapshot, change) => ({ version, md5ext, signal }) => !isAssetName(md5ext) || signal.aborted ? Promise.resolve(null) : serial(() => readAsset(snapshot, change, version, md5ext, signal)).catch(() => null)
|
|
17333
|
+
assetLoader: (snapshot, change) => ({ version, md5ext, signal }) => !isAssetName(md5ext) || signal.aborted ? Promise.resolve(null) : serial(() => readAsset(snapshot, change, version, md5ext, signal)).catch(() => null),
|
|
17334
|
+
/**
|
|
17335
|
+
* Reads the project file of a folder in both versions of a comparison, with each version's assets directory.
|
|
17336
|
+
* `null` stands for a version without the file. Throws `changed` when the repository changed since the snapshot.
|
|
17337
|
+
*/
|
|
17338
|
+
readProject: (snapshot, folder, comparison) => serial(() => readProject(snapshot, folder, comparison)),
|
|
17339
|
+
/** Reads the assets of a folder in both versions of a comparison, like `assetLoader`. */
|
|
17340
|
+
projectAssetLoader: (snapshot, folder, comparison) => ({ version, md5ext, signal }) => !isAssetName(md5ext) || signal.aborted ? Promise.resolve(null) : serial(() => readFolderAsset(snapshot, pathParts(folder), () => comparisonSource(snapshot, comparison, version), md5ext, signal)).catch(() => null)
|
|
17278
17341
|
};
|
|
17279
17342
|
}
|
|
17280
17343
|
export { GitPanelError as n, createReadOnlyFs as r, openRepository as t };
|
package/dist/repository.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export interface Commit {
|
|
|
19
19
|
author: string;
|
|
20
20
|
timestamp: number;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Two versions of a project folder as a person sees them: the last commit and the working tree, so changes split
|
|
24
|
+
* across the index and the working tree read as one; or one commit and its first parent.
|
|
25
|
+
*/
|
|
26
|
+
export type ProjectComparison = {
|
|
27
|
+
kind: 'worktree';
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'commit';
|
|
30
|
+
commit: string;
|
|
31
|
+
};
|
|
22
32
|
type Metadata = Map<string, Uint8Array | null>;
|
|
23
33
|
export interface Snapshot {
|
|
24
34
|
branch: string | undefined;
|
|
@@ -67,5 +77,19 @@ export declare function openRepository(root: FileSystemDirectoryHandle): Promise
|
|
|
67
77
|
* Reads never write, and resolve `null` for anything missing, invalid, larger than 10 MiB or changed since the snapshot.
|
|
68
78
|
*/
|
|
69
79
|
assetLoader: (snapshot: Snapshot, change: FileChange) => ProjectAssetLoader;
|
|
80
|
+
/**
|
|
81
|
+
* Reads the project file of a folder in both versions of a comparison, with each version's assets directory.
|
|
82
|
+
* `null` stands for a version without the file. Throws `changed` when the repository changed since the snapshot.
|
|
83
|
+
*/
|
|
84
|
+
readProject: (snapshot: Snapshot, folder: string, comparison: ProjectComparison) => Promise<{
|
|
85
|
+
before: string | null;
|
|
86
|
+
after: string | null;
|
|
87
|
+
assetsDirectory: {
|
|
88
|
+
before: string;
|
|
89
|
+
after: string;
|
|
90
|
+
};
|
|
91
|
+
}>;
|
|
92
|
+
/** Reads the assets of a folder in both versions of a comparison, like `assetLoader`. */
|
|
93
|
+
projectAssetLoader: (snapshot: Snapshot, folder: string, comparison: ProjectComparison) => ProjectAssetLoader;
|
|
70
94
|
}>;
|
|
71
95
|
export type Repository = Awaited<ReturnType<typeof openRepository>>;
|
package/dist/repository.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { n as GitPanelError, r as createReadOnlyFs, t as openRepository } from "./repository-
|
|
1
|
+
export { n as GitPanelError, r as createReadOnlyFs, t as openRepository } from "./repository-VBp-UNWc.js";
|
package/dist/{scratch-block-diff-renderer-xS8OYsRl.js → scratch-block-diff-renderer-DZR8ep-T.js}
RENAMED
|
@@ -694,18 +694,23 @@ function readAsset(loadAsset, version, md5ext) {
|
|
|
694
694
|
}
|
|
695
695
|
return read;
|
|
696
696
|
}
|
|
697
|
-
/**
|
|
698
|
-
function
|
|
699
|
-
const
|
|
700
|
-
|
|
697
|
+
/** The image type of an asset file name, or `undefined` for sounds and anything that is not an image. */
|
|
698
|
+
function assetImageType(md5ext) {
|
|
699
|
+
const match = /^[0-9A-Za-z_-]{1,128}\.([0-9A-Za-z]{1,8})$/.exec(md5ext);
|
|
700
|
+
return match ? imageTypes[match[1].toLowerCase()] : void 0;
|
|
701
|
+
}
|
|
702
|
+
/** Loads one image asset of one version as an object URL, or `null` while it is unavailable or not an image. */
|
|
703
|
+
function useAssetImage(md5ext, version, loadAsset) {
|
|
704
|
+
const type = md5ext ? assetImageType(md5ext) : void 0;
|
|
705
|
+
const key = md5ext && type && loadAsset ? version + ":" + md5ext : "";
|
|
701
706
|
const [image, setImage] = useState();
|
|
702
707
|
useEffect(() => {
|
|
703
|
-
if (!
|
|
708
|
+
if (!key || !loadAsset) return;
|
|
704
709
|
let active = true;
|
|
705
710
|
let url;
|
|
706
|
-
readAsset(loadAsset, version,
|
|
711
|
+
readAsset(loadAsset, version, md5ext).then((blob) => {
|
|
707
712
|
if (!blob || !active) return;
|
|
708
|
-
url = URL.createObjectURL(new Blob([blob], { type
|
|
713
|
+
url = URL.createObjectURL(new Blob([blob], { type }));
|
|
709
714
|
setImage({
|
|
710
715
|
key,
|
|
711
716
|
url
|
|
@@ -718,6 +723,11 @@ function useCostumeImage(target, version, loadAsset) {
|
|
|
718
723
|
}, [key, loadAsset]);
|
|
719
724
|
return (image === null || image === void 0 ? void 0 : image.key) === key ? image.url : null;
|
|
720
725
|
}
|
|
726
|
+
/** Loads the current costume of one version of a target as an object URL for an image, or `null` while it is unavailable. */
|
|
727
|
+
function useCostumeImage(target, version, loadAsset) {
|
|
728
|
+
const costume = target && loadAsset ? currentCostume(target) : null;
|
|
729
|
+
return useAssetImage(costume === null || costume === void 0 ? void 0 : costume.md5ext, version, loadAsset);
|
|
730
|
+
}
|
|
721
731
|
// src/block-diff-preview.tsx
|
|
722
732
|
init_objectSpread2();
|
|
723
733
|
function createPreviewSync() {
|
|
@@ -968,6 +978,7 @@ function compareProjectForReview(before, after) {
|
|
|
968
978
|
};
|
|
969
979
|
}
|
|
970
980
|
// src/project-source-review.tsx
|
|
981
|
+
init_objectSpread2();
|
|
971
982
|
var ja = {
|
|
972
983
|
review: "変更レビュー",
|
|
973
984
|
fullDescription: "PR 本文を表示",
|
|
@@ -1149,15 +1160,26 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, review
|
|
|
1149
1160
|
}
|
|
1150
1161
|
var countScripts = (comparison) => comparison.targets.reduce((count, target) => count + target.scripts.length, 0);
|
|
1151
1162
|
/** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
|
|
1152
|
-
function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fallback, loadAsset }) {
|
|
1163
|
+
function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fallback, loadAsset, target }) {
|
|
1153
1164
|
const labels = locale.startsWith("ja") ? ja : en;
|
|
1165
|
+
const only = target && JSON.stringify([target.isStage, target.isStage || target.name]);
|
|
1154
1166
|
const comparison = useMemo(() => {
|
|
1167
|
+
let result;
|
|
1155
1168
|
try {
|
|
1156
|
-
|
|
1169
|
+
result = compareProjectForReview(parseProjectSource(before), parseProjectSource(after));
|
|
1157
1170
|
} catch (_unused) {
|
|
1158
1171
|
return null;
|
|
1159
1172
|
}
|
|
1160
|
-
|
|
1173
|
+
if (!only) return result;
|
|
1174
|
+
return {
|
|
1175
|
+
properties: [],
|
|
1176
|
+
targets: result.targets.filter((item) => JSON.stringify([item.isStage, item.isStage || item.name]) === only).map((item) => _objectSpread2(_objectSpread2({}, item), {}, { properties: [] }))
|
|
1177
|
+
};
|
|
1178
|
+
}, [
|
|
1179
|
+
before,
|
|
1180
|
+
after,
|
|
1181
|
+
only
|
|
1182
|
+
]);
|
|
1161
1183
|
if (!comparison) return fallback === void 0 ? /* @__PURE__ */ React.createElement("p", { role: "alert" }, labels.invalid) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", {
|
|
1162
1184
|
role: "alert",
|
|
1163
1185
|
className: "twgit-review-unreadable"
|
|
@@ -1165,7 +1187,7 @@ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fa
|
|
|
1165
1187
|
return /* @__PURE__ */ React.createElement("section", {
|
|
1166
1188
|
className: "twgit-block-diff twgit-review twgit-review-comparison",
|
|
1167
1189
|
"aria-label": labels.comparison
|
|
1168
|
-
}, /* @__PURE__ */ React.createElement("header", { className: "twgit-review-overview" }, /* @__PURE__ */ React.createElement("dl", null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.targets), /* @__PURE__ */ React.createElement("dd", null, comparison.targets.length)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.scripts), /* @__PURE__ */ React.createElement("dd", null, countScripts(comparison))))), /* @__PURE__ */ React.createElement(ComparisonTargets, {
|
|
1190
|
+
}, !only && /* @__PURE__ */ React.createElement("header", { className: "twgit-review-overview" }, /* @__PURE__ */ React.createElement("dl", null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.targets), /* @__PURE__ */ React.createElement("dd", null, comparison.targets.length)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", null, labels.scripts), /* @__PURE__ */ React.createElement("dd", null, countScripts(comparison))))), /* @__PURE__ */ React.createElement(ComparisonTargets, {
|
|
1169
1191
|
comparison,
|
|
1170
1192
|
labels,
|
|
1171
1193
|
renderBlock,
|
|
@@ -1229,6 +1251,236 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
|
|
|
1229
1251
|
}
|
|
1230
1252
|
}))));
|
|
1231
1253
|
}
|
|
1254
|
+
// src/project-change-model.ts
|
|
1255
|
+
init_objectSpread2();
|
|
1256
|
+
var targetDataKeys = [
|
|
1257
|
+
"variables",
|
|
1258
|
+
"lists",
|
|
1259
|
+
"broadcasts"
|
|
1260
|
+
];
|
|
1261
|
+
var nonSettingKeys = /* @__PURE__ */ new Set([
|
|
1262
|
+
"blocks",
|
|
1263
|
+
"costumes",
|
|
1264
|
+
"sounds",
|
|
1265
|
+
...targetDataKeys
|
|
1266
|
+
]);
|
|
1267
|
+
var propertyLabels = /* @__PURE__ */ new Map([
|
|
1268
|
+
["variables", ["変数", "Variables"]],
|
|
1269
|
+
["lists", ["リスト", "Lists"]],
|
|
1270
|
+
["broadcasts", ["メッセージ", "Broadcast messages"]],
|
|
1271
|
+
["costumes", ["コスチューム・背景", "Costumes and backdrops"]],
|
|
1272
|
+
["sounds", ["音", "Sounds"]],
|
|
1273
|
+
["blocks", ["コード", "Code"]],
|
|
1274
|
+
["x", ["X 座標", "X position"]],
|
|
1275
|
+
["y", ["Y 座標", "Y position"]],
|
|
1276
|
+
["size", ["大きさ", "Size"]],
|
|
1277
|
+
["direction", ["向き", "Direction"]],
|
|
1278
|
+
["visible", ["表示状態", "Visibility"]],
|
|
1279
|
+
["currentCostume", ["表示するコスチューム", "Selected costume"]],
|
|
1280
|
+
["layerOrder", ["重なり順", "Layer order"]],
|
|
1281
|
+
["volume", ["音量", "Volume"]],
|
|
1282
|
+
["comments", ["コメント", "Comments"]],
|
|
1283
|
+
["draggable", ["ドラッグ可否", "Draggable"]],
|
|
1284
|
+
["rotationStyle", ["回転方法", "Rotation style"]],
|
|
1285
|
+
["tempo", ["テンポ", "Tempo"]],
|
|
1286
|
+
["videoTransparency", ["ビデオの透明度", "Video transparency"]],
|
|
1287
|
+
["videoState", ["ビデオの状態", "Video state"]],
|
|
1288
|
+
["textToSpeechLanguage", ["音声合成の言語", "Text to speech language"]],
|
|
1289
|
+
["monitors", ["モニター", "Monitors"]],
|
|
1290
|
+
["extensions", ["拡張機能", "Extensions"]],
|
|
1291
|
+
["meta", ["作品の情報", "Project metadata"]]
|
|
1292
|
+
]);
|
|
1293
|
+
/** The Japanese or English name of a project or target property key; unknown keys are returned as they are. */
|
|
1294
|
+
function projectPropertyLabel(key, locale = "en") {
|
|
1295
|
+
var _propertyLabels$get, _propertyLabels$get2;
|
|
1296
|
+
return (_propertyLabels$get = (_propertyLabels$get2 = propertyLabels.get(key)) === null || _propertyLabels$get2 === void 0 ? void 0 : _propertyLabels$get2[locale.startsWith("ja") ? 0 : 1]) !== null && _propertyLabels$get !== void 0 ? _propertyLabels$get : key;
|
|
1297
|
+
}
|
|
1298
|
+
/** The asset file name of a costume or sound entry: `md5ext`, or `assetId.dataFormat`; `''` when it has neither. */
|
|
1299
|
+
function assetFileName(entry) {
|
|
1300
|
+
if (typeof entry.md5ext === "string") return entry.md5ext;
|
|
1301
|
+
return typeof entry.assetId === "string" && typeof entry.dataFormat === "string" ? entry.assetId + "." + entry.dataFormat : "";
|
|
1302
|
+
}
|
|
1303
|
+
function joinPath(...parts) {
|
|
1304
|
+
return parts.flatMap((part) => part.split("/")).filter((part) => part !== "" && part !== ".").join("/");
|
|
1305
|
+
}
|
|
1306
|
+
function statusOf(before, after) {
|
|
1307
|
+
return before === void 0 ? "added" : after === void 0 ? "removed" : "updated";
|
|
1308
|
+
}
|
|
1309
|
+
function entriesOf(target, kind) {
|
|
1310
|
+
const list = target === null || target === void 0 ? void 0 : target[kind];
|
|
1311
|
+
return Array.isArray(list) ? list.filter(isRecord) : [];
|
|
1312
|
+
}
|
|
1313
|
+
function nameOf(entry) {
|
|
1314
|
+
return typeof entry.name === "string" ? entry.name : "";
|
|
1315
|
+
}
|
|
1316
|
+
/** Pairs the entries of two versions by name (repeated names pair in order): after's order first, then removed ones. */
|
|
1317
|
+
function pairEntries(before, after) {
|
|
1318
|
+
var _remaining$get;
|
|
1319
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
1320
|
+
for (const entry of before) {
|
|
1321
|
+
const queue = remaining.get(nameOf(entry));
|
|
1322
|
+
if (queue) queue.push(entry);
|
|
1323
|
+
else remaining.set(nameOf(entry), [entry]);
|
|
1324
|
+
}
|
|
1325
|
+
const pairs = [];
|
|
1326
|
+
for (const entry of after) pairs.push({
|
|
1327
|
+
name: nameOf(entry),
|
|
1328
|
+
before: (_remaining$get = remaining.get(nameOf(entry))) === null || _remaining$get === void 0 ? void 0 : _remaining$get.shift(),
|
|
1329
|
+
after: entry
|
|
1330
|
+
});
|
|
1331
|
+
for (const entry of before) {
|
|
1332
|
+
const queue = remaining.get(nameOf(entry));
|
|
1333
|
+
if ((queue === null || queue === void 0 ? void 0 : queue[0]) === entry) pairs.push({
|
|
1334
|
+
name: nameOf(entry),
|
|
1335
|
+
before: queue.shift()
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
return pairs;
|
|
1339
|
+
}
|
|
1340
|
+
function changedKeys(before, after, skip) {
|
|
1341
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(after), ...Object.keys(before)])].filter((key) => !skip(key) && !equal(before[key], after[key]));
|
|
1342
|
+
}
|
|
1343
|
+
/** Arranges the changed files of one project folder by stage, sprite and element. Pure; reads nothing. */
|
|
1344
|
+
function organizeProjectChanges(input) {
|
|
1345
|
+
const { folder, files, assetsDirectory } = input;
|
|
1346
|
+
const normalize = (project) => project === null ? { targets: [] } : _objectSpread2(_objectSpread2({}, project), {}, { targets: project.targets.map((target) => {
|
|
1347
|
+
var _target$blocks;
|
|
1348
|
+
return _objectSpread2(_objectSpread2({}, target), {}, { blocks: (_target$blocks = target.blocks) !== null && _target$blocks !== void 0 ? _target$blocks : {} });
|
|
1349
|
+
}) });
|
|
1350
|
+
const before = normalize(input.before), after = normalize(input.after);
|
|
1351
|
+
const projectPath = joinPath(folder, "project.source.json");
|
|
1352
|
+
const projectFiles = files.filter((file) => file.path === projectPath);
|
|
1353
|
+
const explained = /* @__PURE__ */ new Set();
|
|
1354
|
+
let projectUsed = false;
|
|
1355
|
+
const useProject = () => {
|
|
1356
|
+
projectUsed = true;
|
|
1357
|
+
return [...projectFiles];
|
|
1358
|
+
};
|
|
1359
|
+
const filesAt = (paths) => {
|
|
1360
|
+
const wanted = new Set(paths.filter((path) => path !== void 0));
|
|
1361
|
+
const found = files.filter((file) => wanted.has(file.path));
|
|
1362
|
+
for (const file of found) explained.add(file.path);
|
|
1363
|
+
return found;
|
|
1364
|
+
};
|
|
1365
|
+
const review = compareProjectForReview(before, after);
|
|
1366
|
+
const scripts = new Map(review.targets.map((target) => {
|
|
1367
|
+
var _target$after;
|
|
1368
|
+
return [targetKey((_target$after = target.after) !== null && _target$after !== void 0 ? _target$after : target.before), target.scripts.length];
|
|
1369
|
+
}));
|
|
1370
|
+
const oldTargets = new Map(before.targets.map((target) => [targetKey(target), target]));
|
|
1371
|
+
const newTargets = new Map(after.targets.map((target) => [targetKey(target), target]));
|
|
1372
|
+
const keys = [...new Set([...after.targets, ...before.targets].map(targetKey))];
|
|
1373
|
+
keys.sort((a, b) => Number(b === "stage") - Number(a === "stage"));
|
|
1374
|
+
const targets = [];
|
|
1375
|
+
for (const key of keys) {
|
|
1376
|
+
var _scripts$get;
|
|
1377
|
+
const a = oldTargets.get(key), b = newTargets.get(key);
|
|
1378
|
+
const status = statusOf(a, b);
|
|
1379
|
+
const elements = [];
|
|
1380
|
+
const count = (_scripts$get = scripts.get(key)) !== null && _scripts$get !== void 0 ? _scripts$get : 0;
|
|
1381
|
+
if (count) elements.push({
|
|
1382
|
+
key: JSON.stringify([
|
|
1383
|
+
key,
|
|
1384
|
+
"code",
|
|
1385
|
+
""
|
|
1386
|
+
]),
|
|
1387
|
+
kind: "code",
|
|
1388
|
+
status,
|
|
1389
|
+
scripts: count,
|
|
1390
|
+
files: useProject()
|
|
1391
|
+
});
|
|
1392
|
+
for (const kind of ["costumes", "sounds"]) {
|
|
1393
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1394
|
+
for (const pair of pairEntries(entriesOf(a, kind), entriesOf(b, kind))) {
|
|
1395
|
+
var _seen$get;
|
|
1396
|
+
const md5ext = {
|
|
1397
|
+
before: pair.before && assetFileName(pair.before) || void 0,
|
|
1398
|
+
after: pair.after && assetFileName(pair.after) || void 0
|
|
1399
|
+
};
|
|
1400
|
+
const differs = !equal(pair.before, pair.after);
|
|
1401
|
+
const assets = filesAt([md5ext.before && joinPath(folder, assetsDirectory.before, md5ext.before), md5ext.after && joinPath(folder, assetsDirectory.after, md5ext.after)]);
|
|
1402
|
+
if (!differs && !assets.length) continue;
|
|
1403
|
+
const repeat = (_seen$get = seen.get(pair.name)) !== null && _seen$get !== void 0 ? _seen$get : 0;
|
|
1404
|
+
seen.set(pair.name, repeat + 1);
|
|
1405
|
+
elements.push({
|
|
1406
|
+
key: JSON.stringify(repeat ? [
|
|
1407
|
+
key,
|
|
1408
|
+
kind,
|
|
1409
|
+
pair.name,
|
|
1410
|
+
repeat
|
|
1411
|
+
] : [
|
|
1412
|
+
key,
|
|
1413
|
+
kind,
|
|
1414
|
+
pair.name
|
|
1415
|
+
]),
|
|
1416
|
+
kind,
|
|
1417
|
+
status: statusOf(pair.before, pair.after),
|
|
1418
|
+
name: pair.name,
|
|
1419
|
+
before: pair.before,
|
|
1420
|
+
after: pair.after,
|
|
1421
|
+
md5ext,
|
|
1422
|
+
files: [...differs ? useProject() : [], ...assets]
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (a && b) {
|
|
1427
|
+
const oldFields = a, newFields = b;
|
|
1428
|
+
const data = changedKeys(oldFields, newFields, (property) => !targetDataKeys.includes(property));
|
|
1429
|
+
if (data.length) elements.push({
|
|
1430
|
+
key: JSON.stringify([
|
|
1431
|
+
key,
|
|
1432
|
+
"data",
|
|
1433
|
+
""
|
|
1434
|
+
]),
|
|
1435
|
+
kind: "data",
|
|
1436
|
+
status,
|
|
1437
|
+
properties: data,
|
|
1438
|
+
files: useProject()
|
|
1439
|
+
});
|
|
1440
|
+
const settings = changedKeys(oldFields, newFields, (property) => nonSettingKeys.has(property));
|
|
1441
|
+
if (settings.length) elements.push({
|
|
1442
|
+
key: JSON.stringify([
|
|
1443
|
+
key,
|
|
1444
|
+
"settings",
|
|
1445
|
+
""
|
|
1446
|
+
]),
|
|
1447
|
+
kind: "settings",
|
|
1448
|
+
status,
|
|
1449
|
+
properties: settings,
|
|
1450
|
+
files: useProject()
|
|
1451
|
+
});
|
|
1452
|
+
} else projectUsed = true;
|
|
1453
|
+
if (status !== "updated" || elements.length) {
|
|
1454
|
+
const { name, isStage } = b !== null && b !== void 0 ? b : a;
|
|
1455
|
+
targets.push({
|
|
1456
|
+
key,
|
|
1457
|
+
name,
|
|
1458
|
+
isStage,
|
|
1459
|
+
status,
|
|
1460
|
+
before: a,
|
|
1461
|
+
after: b,
|
|
1462
|
+
elements
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const project = changedKeys(before, after, (key) => key === "targets").map((property) => ({
|
|
1467
|
+
key: JSON.stringify([
|
|
1468
|
+
"project",
|
|
1469
|
+
"settings",
|
|
1470
|
+
property
|
|
1471
|
+
]),
|
|
1472
|
+
kind: "settings",
|
|
1473
|
+
status: statusOf(before[property], after[property]),
|
|
1474
|
+
properties: [property],
|
|
1475
|
+
files: useProject()
|
|
1476
|
+
}));
|
|
1477
|
+
if (projectUsed) explained.add(projectPath);
|
|
1478
|
+
return {
|
|
1479
|
+
targets,
|
|
1480
|
+
project,
|
|
1481
|
+
other: files.filter((file) => !explained.has(file.path))
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1232
1484
|
// src/github-pull-requests.ts
|
|
1233
1485
|
init_objectSpread2();
|
|
1234
1486
|
var PullRequestError = class extends Error {
|
|
@@ -1827,4 +2079,4 @@ function createScratchBlocksDiffRenderer(options) {
|
|
|
1827
2079
|
};
|
|
1828
2080
|
};
|
|
1829
2081
|
}
|
|
1830
|
-
export { gitHubRepositoryFromOrigin as a,
|
|
2082
|
+
export { compareProjectBlocks as _, gitHubRepositoryFromOrigin as a, readProjectSourceDiff as b, projectPropertyLabel as c, BlockPreview as d, createPreviewSync as f, useCostumeImage as g, useAssetImage as h, createGitHubPullRequests as i, ProjectSourceComparison as l, currentCostume as m, createScratchBlocksDiffLocator as n, assetFileName as o, usePreviewSync as p, PullRequestError as r, organizeProjectChanges as s, createScratchBlocksDiffRenderer as t, ProjectSourceReview as u, createProjectSourceDiff as v, parseProjectSource as y };
|
package/dist/style.css
CHANGED
|
@@ -135,4 +135,21 @@
|
|
|
135
135
|
.twgit-view-switch { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 10px; }
|
|
136
136
|
.twgit-review-unreadable { margin: 6px 0; padding: 8px; background: color-mix(in srgb, #d9822b 16%, Canvas); }
|
|
137
137
|
.twgit-review-comparison .twgit-review-overview dl { margin: 4px 0 8px; }
|
|
138
|
+
.twgit-element-list > section { margin: 6px 0 10px; }
|
|
139
|
+
.twgit-element-target { display: flex; align-items: center; gap: 6px; margin: 10px 0 4px; font-size: 14px; }
|
|
140
|
+
.twgit-element-target img, .twgit-element img, .twgit-element-thumbnail { width: 28px; height: 28px; object-fit: contain; flex: none; }
|
|
141
|
+
.twgit-element-thumbnail { display: inline-block; border-radius: 4px; background: color-mix(in srgb, CanvasText 8%, Canvas); }
|
|
142
|
+
.twgit-panel .twgit-element { display: flex; align-items: center; gap: 6px; padding-inline-start: 10px; }
|
|
143
|
+
.twgit-element-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
144
|
+
.twgit-file-list { margin-top: 10px; }
|
|
145
|
+
.twgit-element-pictures { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
146
|
+
.twgit-element-picture { margin: 0; padding: 10px; border: 1px solid color-mix(in srgb, CanvasText 18%, Canvas); border-radius: 6px; }
|
|
147
|
+
.twgit-element-picture img { display: block; max-width: 100%; max-height: 260px; margin: 8px auto 0; object-fit: contain;
|
|
148
|
+
background: repeating-conic-gradient(color-mix(in srgb, CanvasText 6%, Canvas) 0 25%, Canvas 0 50%) 0 0 / 16px 16px; }
|
|
149
|
+
.twgit-element-values { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
150
|
+
.twgit-element-values th, .twgit-element-values td { text-align: start; vertical-align: top; padding: 6px 8px;
|
|
151
|
+
border-bottom: 1px solid color-mix(in srgb, CanvasText 15%, Canvas); overflow-wrap: anywhere; }
|
|
152
|
+
.twgit-element-values td { font-family: ui-monospace, monospace; }
|
|
153
|
+
.twgit-element-files { margin-top: 12px; }
|
|
154
|
+
.twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
|
|
138
155
|
/*$vite$:1*/
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
|
2
2
|
import {render, unmountComponentAtNode} from 'react-dom';
|
|
3
3
|
import {FileDiffView} from './file-diff-view.js';
|
|
4
|
+
import {findElement, ProjectChangeListView, ProjectElementView, projectChangeLabels, useProjectChanges} from './project-change-list.js';
|
|
5
|
+
import type {ChangedFile} from './project-change-model.js';
|
|
4
6
|
import {openRepository} from './repository.js';
|
|
5
7
|
import type {Repository, Snapshot, FileChange, FileDiff, ChangeGroup} from './repository.js';
|
|
6
8
|
import {labelsFor} from './messages.js';
|
|
@@ -214,6 +216,20 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
214
216
|
});
|
|
215
217
|
const selectFile = (selected: FileChange) => run(async () => ({selected,
|
|
216
218
|
diff: await state.repository!.readDiff(state.snapshot!, selected)}));
|
|
219
|
+
// A selected stage or sprite element replaces the file diff; picking a file goes back to it.
|
|
220
|
+
const [element, setElement] = useState<string | null>(null);
|
|
221
|
+
const [fileListOpen, setFileListOpen] = useState(false);
|
|
222
|
+
const projectChanges = useProjectChanges(state.repository, state.snapshot, state.project, state.commit, state.changes);
|
|
223
|
+
const openFile = (file: ChangedFile | FileChange) => {
|
|
224
|
+
const change = state.changes.find(item => item.path === file.path && item.group === file.group);
|
|
225
|
+
if (!change) return;
|
|
226
|
+
setElement(null);
|
|
227
|
+
void selectFile(change);
|
|
228
|
+
};
|
|
229
|
+
const openElement = (key: string) => {
|
|
230
|
+
setElement(key);
|
|
231
|
+
setState(previous => ({...previous, selected: null, diff: null}));
|
|
232
|
+
};
|
|
217
233
|
const selectCommit = (commit: string) => run(async () => ({commit,
|
|
218
234
|
changes: await state.repository!.historyChanges(state.snapshot!, commit), selected: null, diff: null}));
|
|
219
235
|
const remoteOperation = async (operation: RemoteOperation) => {
|
|
@@ -254,6 +270,8 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
254
270
|
state.repository.assetLoader(state.snapshot, state.selected) : undefined, [state.repository, state.snapshot, state.selected]);
|
|
255
271
|
if (!visible) return null;
|
|
256
272
|
const groups: ChangeGroup[] = state.commit ? ['commit'] : ['staged', 'unstaged', 'untracked'];
|
|
273
|
+
const projectList = typeof projectChanges === 'object' ? projectChanges : null;
|
|
274
|
+
const selection = projectList && !state.selected ? findElement(projectList.list, element) : null;
|
|
257
275
|
return <div className="twgit-backdrop">
|
|
258
276
|
<div className="twgit-panel" ref={dialog} tabIndex={-1} role="dialog" aria-modal="true" aria-label={labels.title}
|
|
259
277
|
onKeyDown={focusTrap}>
|
|
@@ -297,15 +315,22 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
297
315
|
onClick={() => void browse([state.browser!.path, name].filter(Boolean).join('/'))}>{name}/</button></li>)}</ul>
|
|
298
316
|
</details>}
|
|
299
317
|
<h3><button disabled={state.busy} onClick={() => void controller.refresh()}>{labels.worktree}</button></h3>
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
318
|
+
{projectChanges === 'loading' && <p role="status">{projectChangeLabels(locale).loading}</p>}
|
|
319
|
+
{projectChanges === 'unreadable' && <p>{projectChangeLabels(locale).unreadable}</p>}
|
|
320
|
+
{(() => {
|
|
321
|
+
const fileList = groups.map(group => <section key={group}>
|
|
322
|
+
<h3>{labels[group]} · {state.changes.filter(change => change.group === group).length}</h3>
|
|
323
|
+
<ul>{state.changes.filter(change => change.group === group).map(change => <li key={change.path}>
|
|
324
|
+
<button className="twgit-file" data-git-group={group} disabled={state.busy}
|
|
325
|
+
aria-pressed={state.selected === change} onClick={() => openFile(change)}>
|
|
326
|
+
<span>{!change.after ? 'D' : !change.before ? 'A' : 'M'}</span> {change.path}
|
|
327
|
+
</button>
|
|
328
|
+
</li>)}</ul>
|
|
329
|
+
</section>);
|
|
330
|
+
return projectList ? <ProjectChangeListView changes={projectList} selected={selection ? element : null}
|
|
331
|
+
locale={locale} busy={state.busy} onSelect={openElement} onSelectFile={openFile} fileList={fileList}
|
|
332
|
+
fileListOpen={fileListOpen} setFileListOpen={setFileListOpen}/> : fileList;
|
|
333
|
+
})()}
|
|
309
334
|
{!state.changes.length && <p>{labels.clean}</p>}
|
|
310
335
|
<details><summary>{labels.branches}</summary><ul>{state.snapshot?.branches.map(branch => <li key={branch}>{branch}</li>)}</ul></details>
|
|
311
336
|
<h3>{labels.history}</h3>
|
|
@@ -316,8 +341,10 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
316
341
|
</li>)}</ol>
|
|
317
342
|
</aside>
|
|
318
343
|
<main className="twgit-diff">
|
|
319
|
-
{
|
|
320
|
-
renderBlock={host.renderReviewBlock}
|
|
344
|
+
{selection && projectList ? <ProjectElementView changes={projectList} selection={selection} locale={locale}
|
|
345
|
+
renderBlock={host.renderReviewBlock} onSelectFile={openFile}/> :
|
|
346
|
+
state.selected ? <FileDiffView change={state.selected} diff={state.diff} labels={labels} locale={locale}
|
|
347
|
+
renderBlock={host.renderReviewBlock} loadAsset={loadAsset}/> : <p>{labels.select}</p>}
|
|
321
348
|
</main>
|
|
322
349
|
</div>}
|
|
323
350
|
<footer>{labels.permissions}</footer>
|
package/src/project-assets.ts
CHANGED
|
@@ -43,18 +43,24 @@ function readAsset(loadAsset: ProjectAssetLoader, version: ProjectVersion, md5ex
|
|
|
43
43
|
return read;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
export function
|
|
48
|
-
const
|
|
49
|
-
|
|
46
|
+
/** The image type of an asset file name, or `undefined` for sounds and anything that is not an image. */
|
|
47
|
+
export function assetImageType(md5ext: string): string | undefined {
|
|
48
|
+
const match = /^[0-9A-Za-z_-]{1,128}\.([0-9A-Za-z]{1,8})$/.exec(md5ext);
|
|
49
|
+
return match ? imageTypes[match[1].toLowerCase()] : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Loads one image asset of one version as an object URL, or `null` while it is unavailable or not an image. */
|
|
53
|
+
export function useAssetImage(md5ext: string | undefined, version: ProjectVersion, loadAsset?: ProjectAssetLoader): string | null {
|
|
54
|
+
const type = md5ext ? assetImageType(md5ext) : undefined;
|
|
55
|
+
const key = md5ext && type && loadAsset ? version + ':' + md5ext : '';
|
|
50
56
|
const [image, setImage] = useState<{key: string; url: string}>();
|
|
51
57
|
useEffect(() => {
|
|
52
|
-
if (!
|
|
58
|
+
if (!key || !loadAsset) return;
|
|
53
59
|
let active = true;
|
|
54
60
|
let url: string | undefined;
|
|
55
|
-
void readAsset(loadAsset, version,
|
|
61
|
+
void readAsset(loadAsset, version, md5ext!).then(blob => {
|
|
56
62
|
if (!blob || !active) return;
|
|
57
|
-
url = URL.createObjectURL(new Blob([blob], {type
|
|
63
|
+
url = URL.createObjectURL(new Blob([blob], {type}));
|
|
58
64
|
setImage({key, url});
|
|
59
65
|
});
|
|
60
66
|
return () => {
|
|
@@ -64,3 +70,9 @@ export function useCostumeImage(target: ProjectTarget | undefined, version: Proj
|
|
|
64
70
|
}, [key, loadAsset]);
|
|
65
71
|
return image?.key === key ? image.url : null;
|
|
66
72
|
}
|
|
73
|
+
|
|
74
|
+
/** Loads the current costume of one version of a target as an object URL for an image, or `null` while it is unavailable. */
|
|
75
|
+
export function useCostumeImage(target: ProjectTarget | undefined, version: ProjectVersion, loadAsset?: ProjectAssetLoader): string | null {
|
|
76
|
+
const costume = target && loadAsset ? currentCostume(target) : null;
|
|
77
|
+
return useAssetImage(costume?.md5ext, version, loadAsset);
|
|
78
|
+
}
|