@kubohiroya/turbowarp-plus-editor-git 0.8.0 → 0.10.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 +459 -74
- package/dist/messages.d.ts +34 -0
- 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-CtvWYPQ8.js} +187 -8
- package/dist/repository.d.ts +41 -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 +30 -2
- package/package.json +1 -1
- package/src/connection-settings.tsx +14 -8
- package/src/file-diff-view.tsx +4 -2
- package/src/index.tsx +68 -23
- package/src/messages.ts +34 -26
- 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 +129 -7
- package/src/style.css +30 -2
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
|
@@ -116,14 +116,24 @@
|
|
|
116
116
|
.twgit-panel button:focus-visible, .twgit-panel summary:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
|
|
117
117
|
.twgit-panel button[aria-pressed="true"] { color: HighlightText; background: Highlight; }
|
|
118
118
|
.twgit-context { display: flex; gap: 8px 20px; flex-wrap: wrap; padding: 8px 16px; }
|
|
119
|
-
.twgit-
|
|
119
|
+
.twgit-context { align-items: center; }
|
|
120
|
+
.twgit-connection, .twgit-git-details { position: relative; }
|
|
121
|
+
.twgit-connection > summary, .twgit-git-details > summary { cursor: pointer; }
|
|
122
|
+
/* Settings and Git details open over the list, so the list and the diff stay where they are. */
|
|
123
|
+
.twgit-connection-body { position: absolute; z-index: 5; top: calc(100% + 4px); left: 0; width: min(440px, 80vw); max-height: 60vh;
|
|
124
|
+
overflow: auto; padding: 10px 12px; background: Canvas; color: CanvasText; border: 1px solid GrayText; border-radius: 6px;
|
|
125
|
+
box-shadow: 0 8px 24px #0004; }
|
|
126
|
+
.twgit-connection-body > p { margin: 4px 0; overflow-wrap: anywhere; }
|
|
127
|
+
.twgit-sync { display: inline-flex; flex-wrap: wrap; gap: 4px; }
|
|
128
|
+
.twgit-change-kind { display: inline-block; min-width: 2.6em; font-size: 11px; opacity: .8; }
|
|
129
|
+
.twgit-file-details { font-size: 12px; margin-bottom: 6px; }
|
|
130
|
+
.twgit-file-details > p { margin: 2px 0; }
|
|
120
131
|
.twgit-content { display: grid; grid-template-columns: minmax(230px, 30%) minmax(0, 1fr); flex: 1; min-height: 0; }
|
|
121
132
|
.twgit-panel aside { padding: 0 14px 14px; overflow: auto; border-right: 1px solid GrayText; }
|
|
122
133
|
.twgit-panel aside ul { list-style: none; padding: 0; margin: 4px 0; }
|
|
123
134
|
.twgit-panel aside li { overflow-wrap: anywhere; }
|
|
124
135
|
.twgit-panel summary { cursor: pointer; }
|
|
125
136
|
.twgit-panel .twgit-file { width: 100%; border: 0; margin: 1px 0; }
|
|
126
|
-
.twgit-file span { display: inline-block; width: 1.4em; font-family: monospace; }
|
|
127
137
|
.twgit-diff { padding: 0 16px 16px; min-width: 0; overflow: auto; }
|
|
128
138
|
.twgit-diff .diff { font-size: 12px; }
|
|
129
139
|
.twgit-history { list-style: none; margin: 0; padding: 0; }
|
|
@@ -135,4 +145,22 @@
|
|
|
135
145
|
.twgit-view-switch { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 10px; }
|
|
136
146
|
.twgit-review-unreadable { margin: 6px 0; padding: 8px; background: color-mix(in srgb, #d9822b 16%, Canvas); }
|
|
137
147
|
.twgit-review-comparison .twgit-review-overview dl { margin: 4px 0 8px; }
|
|
148
|
+
.twgit-element-list > section { margin: 6px 0 10px; }
|
|
149
|
+
.twgit-element-target { display: flex; align-items: center; gap: 6px; margin: 10px 0 4px; font-size: 14px; }
|
|
150
|
+
.twgit-element-target img, .twgit-element img, .twgit-element-thumbnail { width: 28px; height: 28px; object-fit: contain; flex: none; }
|
|
151
|
+
.twgit-element-thumbnail { display: inline-block; border-radius: 4px; background: color-mix(in srgb, CanvasText 8%, Canvas); }
|
|
152
|
+
.twgit-panel .twgit-element { display: flex; align-items: center; gap: 6px; padding-inline-start: 10px; }
|
|
153
|
+
.twgit-element-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
154
|
+
.twgit-file-list { margin-top: 10px; }
|
|
155
|
+
.twgit-element-pictures { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
156
|
+
.twgit-element-picture { margin: 0; padding: 10px; border: 1px solid color-mix(in srgb, CanvasText 18%, Canvas); border-radius: 6px; }
|
|
157
|
+
.twgit-element-picture img { display: block; max-width: 100%; max-height: 260px; margin: 8px auto 0; object-fit: contain;
|
|
158
|
+
background: repeating-conic-gradient(color-mix(in srgb, CanvasText 6%, Canvas) 0 25%, Canvas 0 50%) 0 0 / 16px 16px; }
|
|
159
|
+
.twgit-element-values { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
160
|
+
.twgit-element-values th, .twgit-element-values td { text-align: start; vertical-align: top; padding: 6px 8px;
|
|
161
|
+
border-bottom: 1px solid color-mix(in srgb, CanvasText 15%, Canvas); overflow-wrap: anywhere; }
|
|
162
|
+
.twgit-element-values td { font-family: ui-monospace, monospace; }
|
|
163
|
+
.twgit-element-files { margin-top: 12px; }
|
|
164
|
+
.twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
|
|
165
|
+
.twgit-git-details > .twgit-connection-body { left: auto; right: 0; }
|
|
138
166
|
/*$vite$:1*/
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@ const en = {
|
|
|
28
28
|
unavailable: 'No GitHub connection is configured.', changed: 'Switching disconnects the current session.',
|
|
29
29
|
error: 'GitHub connection failed. Retry using the selected connection.',
|
|
30
30
|
invalid: 'The selected repository needs a GitHub HTTPS origin.',
|
|
31
|
-
allowPush: '
|
|
31
|
+
allowPush: 'Allow sending to GitHub (push)', fetch: 'Check GitHub (fetch)', pull: 'Get from GitHub (pull)', push: 'Send to GitHub (push)'
|
|
32
32
|
};
|
|
33
33
|
const ja: typeof en = {
|
|
34
34
|
title: 'GitHub 接続', mode: '接続方式', select: '接続方式を選択',
|
|
@@ -39,7 +39,7 @@ const ja: typeof en = {
|
|
|
39
39
|
unavailable: 'GitHub 接続先が設定されていません。', changed: '方式を切り替えると現在のセッションを切断します。',
|
|
40
40
|
error: 'GitHub 接続に失敗しました。選択中の方式で再試行してください。',
|
|
41
41
|
invalid: '選択したリポジトリには GitHub の HTTPS リモート origin が必要です。',
|
|
42
|
-
allowPush: 'push
|
|
42
|
+
allowPush: 'GitHub に送ることを許可する(push)', fetch: 'GitHub と確かめる(fetch)', pull: 'GitHub から取り込む(pull)', push: 'GitHub に送る(push)'
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
export function ConnectionSettings({settings, locale, readOrigin, runOperation}: {
|
|
@@ -69,8 +69,15 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
|
|
|
69
69
|
if (!remote) throw new Error('INVALID_REMOTE');
|
|
70
70
|
await settings.connect(remote, allowPush);
|
|
71
71
|
});
|
|
72
|
-
|
|
72
|
+
const connected = !!mode && state.status === 'connected';
|
|
73
|
+
// Sync sits beside the GitHub status; the settings open over the list instead of pushing it down.
|
|
74
|
+
return <>
|
|
75
|
+
{runOperation && connected && <span className="twgit-sync">{(['fetch', 'pull', 'push'] as const).map(operation =>
|
|
76
|
+
<button key={operation} disabled={busy || !readOrigin || (operation === 'push' && !write)}
|
|
77
|
+
onClick={() => void run(() => runOperation(operation))}>{labels[operation]}</button>)}</span>}
|
|
78
|
+
<details className="twgit-connection">
|
|
73
79
|
<summary>{labels.title}</summary>
|
|
80
|
+
<div className="twgit-connection-body">
|
|
74
81
|
{!settings.modes.length ? <p>{labels.unavailable}</p> : <>
|
|
75
82
|
<label>{labels.mode} <select value={mode ?? ''} disabled={busy}
|
|
76
83
|
onChange={event => {const choice = event.currentTarget.value as Mode; void run(() => settings.select(choice));}}>
|
|
@@ -93,11 +100,10 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
|
|
|
93
100
|
{state.status !== 'disconnected' && <button disabled={busy}
|
|
94
101
|
onClick={() => void run(settings.refresh)}>{labels.check}</button>}
|
|
95
102
|
<button disabled={busy} onClick={() => void run(settings.disconnect)}>{labels.disconnect}</button>
|
|
96
|
-
{runOperation && state.status === 'connected' && (['fetch', 'pull', 'push'] as const).map(operation =>
|
|
97
|
-
<button key={operation} disabled={busy || !readOrigin || (operation === 'push' && !write)}
|
|
98
|
-
onClick={() => void run(() => runOperation(operation))}>{labels[operation]}</button>)}
|
|
99
103
|
</>}
|
|
100
|
-
{error && <p className="twgit-error" role="alert">{labels[error]}</p>}
|
|
101
104
|
</>}
|
|
102
|
-
|
|
105
|
+
</div>
|
|
106
|
+
</details>
|
|
107
|
+
{error && <p className="twgit-error" role="alert">{labels[error]}</p>}
|
|
108
|
+
</>;
|
|
103
109
|
}
|
package/src/file-diff-view.tsx
CHANGED
|
@@ -23,8 +23,10 @@ export function FileDiffView({change, diff, labels, locale, renderBlock, loadAss
|
|
|
23
23
|
const names = locale.startsWith('ja') ? ja : en;
|
|
24
24
|
const parsed = diff?.patch ? parseDiff(diff.patch)[0] : null;
|
|
25
25
|
const text = <>
|
|
26
|
-
<
|
|
27
|
-
|
|
26
|
+
<details className="twgit-file-details"><summary>{labels.details}</summary>
|
|
27
|
+
<p>{labels.mode} · {change.oldMode?.toString(8) || '—'} → {change.newMode?.toString(8) || '—'}</p>
|
|
28
|
+
{diff && <p>{diff.beforeSize} → {diff.afterSize} {labels.bytes}</p>}
|
|
29
|
+
</details>
|
|
28
30
|
{diff?.binary && <p>{labels.binary}</p>}
|
|
29
31
|
{diff?.tooLarge && <p>{labels.large}</p>}
|
|
30
32
|
{parsed && <Diff viewType="unified" diffType={parsed.type} hunks={parsed.hunks}>
|
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}>
|
|
@@ -269,17 +287,26 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
269
287
|
<div className="twgit-context">
|
|
270
288
|
<span>{labels.repository} · {state.repository?.root.name || '—'}</span>
|
|
271
289
|
<span>{labels.project} · {state.project === null ? labels.none : state.project || labels.root}</span>
|
|
272
|
-
<strong>{state.snapshot?.branch || (state.snapshot?.head ? labels.detached : labels.unborn)}</strong>
|
|
273
|
-
<code>{state.snapshot?.head?.slice(0, 12)}</code>
|
|
274
290
|
<span data-git-dirty="">{dirty ? labels.dirty : labels.saved}</span>
|
|
291
|
+
{state.snapshot && <span data-git-upstream={state.snapshot.upstream.state} title={labels.checkedAt}>{upstreamText(state.snapshot, labels)}</span>}
|
|
292
|
+
{!review && host.connectionSettings && <ConnectionSettings settings={host.connectionSettings} locale={locale}
|
|
293
|
+
runOperation={host.connectionSettings.getSession ? remoteOperation : undefined}
|
|
294
|
+
readOrigin={state.repository && !state.busy ? async () => {
|
|
295
|
+
const ticket = epoch.current;
|
|
296
|
+
const remote = await state.repository!.readOrigin();
|
|
297
|
+
return epoch.current === ticket ? remote : undefined;
|
|
298
|
+
} : undefined}/>}
|
|
299
|
+
{state.snapshot && <details className="twgit-git-details">
|
|
300
|
+
<summary>{labels.details}</summary>
|
|
301
|
+
<div className="twgit-connection-body">
|
|
302
|
+
<p>{labels.branch} · <strong>{state.snapshot.branch || (state.snapshot.head ? labels.detached : labels.unborn)}</strong></p>
|
|
303
|
+
<p>{labels.head} · <code>{state.snapshot.head?.slice(0, 12) ?? '—'}</code></p>
|
|
304
|
+
<p>{labels.branches} · {state.snapshot.branches.join(', ') || '—'}</p>
|
|
305
|
+
<p>{labels.groups} · {labels.gitStaged} = {labels.staged} / {labels.gitUnstaged} = {labels.unstaged} / {labels.gitUntracked} = {labels.untracked}</p>
|
|
306
|
+
<p>{labels.permissions}</p>
|
|
307
|
+
</div>
|
|
308
|
+
</details>}
|
|
275
309
|
</div>
|
|
276
|
-
{!review && host.connectionSettings && <ConnectionSettings settings={host.connectionSettings} locale={locale}
|
|
277
|
-
runOperation={host.connectionSettings.getSession ? remoteOperation : undefined}
|
|
278
|
-
readOrigin={state.repository && !state.busy ? async () => {
|
|
279
|
-
const ticket = epoch.current;
|
|
280
|
-
const remote = await state.repository!.readOrigin();
|
|
281
|
-
return epoch.current === ticket ? remote : undefined;
|
|
282
|
-
} : undefined}/>}
|
|
283
310
|
{state.error && <p className="twgit-error" role="alert">{state.error}</p>}
|
|
284
311
|
{state.notice && <p className="twgit-context" role="status">{state.notice}</p>}
|
|
285
312
|
{state.busy && <p role="status">{labels.loading}</p>}
|
|
@@ -297,34 +324,52 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
297
324
|
onClick={() => void browse([state.browser!.path, name].filter(Boolean).join('/'))}>{name}/</button></li>)}</ul>
|
|
298
325
|
</details>}
|
|
299
326
|
<h3><button disabled={state.busy} onClick={() => void controller.refresh()}>{labels.worktree}</button></h3>
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
327
|
+
{projectChanges === 'loading' && <p role="status">{projectChangeLabels(locale).loading}</p>}
|
|
328
|
+
{projectChanges === 'unreadable' && <p>{projectChangeLabels(locale).unreadable}</p>}
|
|
329
|
+
{(() => {
|
|
330
|
+
const fileList = groups.map(group => <section key={group}>
|
|
331
|
+
<h3>{labels[group]} · {state.changes.filter(change => change.group === group).length}</h3>
|
|
332
|
+
<ul>{state.changes.filter(change => change.group === group).map(change => <li key={change.path}>
|
|
333
|
+
<button className="twgit-file" data-git-group={group} disabled={state.busy}
|
|
334
|
+
aria-pressed={state.selected === change} onClick={() => openFile(change)}>
|
|
335
|
+
<small className="twgit-change-kind">{!change.after ? labels.deleted : !change.before ? labels.added : labels.modified}</small> {change.path}
|
|
336
|
+
</button>
|
|
337
|
+
</li>)}</ul>
|
|
338
|
+
</section>);
|
|
339
|
+
return projectList ? <ProjectChangeListView changes={projectList} selected={selection ? element : null}
|
|
340
|
+
locale={locale} busy={state.busy} onSelect={openElement} onSelectFile={openFile} fileList={fileList}
|
|
341
|
+
fileListOpen={fileListOpen} setFileListOpen={setFileListOpen}/> : fileList;
|
|
342
|
+
})()}
|
|
309
343
|
{!state.changes.length && <p>{labels.clean}</p>}
|
|
310
|
-
<details><summary>{labels.branches}</summary><ul>{state.snapshot?.branches.map(branch => <li key={branch}>{branch}</li>)}</ul></details>
|
|
311
344
|
<h3>{labels.history}</h3>
|
|
312
345
|
<ol className="twgit-history">{state.snapshot?.commits.map(commit => <li key={commit.oid}>
|
|
313
346
|
<button disabled={state.busy} aria-pressed={state.commit === commit.oid} title={commit.oid}
|
|
314
347
|
onClick={() => void selectCommit(commit.oid)}>{commit.message.split('\n')[0]}</button>
|
|
315
|
-
<small
|
|
348
|
+
<small>{commit.author} · {new Date(commit.timestamp * 1000).toLocaleString(locale)}</small>
|
|
316
349
|
</li>)}</ol>
|
|
317
350
|
</aside>
|
|
318
351
|
<main className="twgit-diff">
|
|
319
|
-
{
|
|
320
|
-
renderBlock={host.renderReviewBlock}
|
|
352
|
+
{selection && projectList ? <ProjectElementView changes={projectList} selection={selection} locale={locale}
|
|
353
|
+
renderBlock={host.renderReviewBlock} onSelectFile={openFile}/> :
|
|
354
|
+
state.selected ? <FileDiffView change={state.selected} diff={state.diff} labels={labels} locale={locale}
|
|
355
|
+
renderBlock={host.renderReviewBlock} loadAsset={loadAsset}/> : <p>{labels.select}</p>}
|
|
321
356
|
</main>
|
|
322
357
|
</div>}
|
|
323
|
-
<footer>{labels.permissions}</footer>
|
|
324
358
|
</div>
|
|
325
359
|
</div>;
|
|
326
360
|
}
|
|
327
361
|
|
|
362
|
+
/** The GitHub relation in plain words, with the number of records where it helps. */
|
|
363
|
+
function upstreamText({upstream}: Snapshot, labels: ReturnType<typeof labelsFor>) {
|
|
364
|
+
switch (upstream.state) {
|
|
365
|
+
case 'same': return labels.upstreamSame;
|
|
366
|
+
case 'ahead': return `${labels.upstreamAhead} · ${upstream.ahead} ${labels.records}`;
|
|
367
|
+
case 'behind': return `${labels.upstreamBehind} · ${upstream.behind} ${labels.records}`;
|
|
368
|
+
case 'diverged': return `${labels.upstreamDiverged} · +${upstream.ahead} / −${upstream.behind}`;
|
|
369
|
+
default: return labels.upstreamUnknown;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
328
373
|
export function mountGitPanel({vm, document, host}: {vm: object; document: Document; host: GitHost}) {
|
|
329
374
|
if (!(document.defaultView as PickerWindow)?.showDirectoryPicker) return null;
|
|
330
375
|
const root = document.createElement('div');
|