@jxsuite/studio 10.2.0 → 10.3.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/chunks/{2020-bx702wqa.js → 2020-906vx2dq.js} +3 -3
- package/dist/chunks/adapter-hnvad61x.js +4 -0
- package/dist/chunks/add-new-flow-3tkn72ey.js +4 -0
- package/dist/chunks/cf-account-picker-xb8vvt2h.js +4 -0
- package/dist/chunks/cf-settings-mkbcz4z2.js +4 -0
- package/dist/chunks/components-w5zqwva1.js +4 -0
- package/dist/chunks/content-source-1vzxq1ba.js +4 -0
- package/dist/chunks/convert-to-repeater-gwzh9gar.js +4 -0
- package/dist/chunks/editor-wvf5e6j9.js +4 -0
- package/dist/chunks/entry-commands-mx2whrjx.js +4 -0
- package/dist/chunks/extension-sections-338ee35t.js +4 -0
- package/dist/chunks/file-ops-9bxvk59j.js +4 -0
- package/dist/chunks/files-t07yqqej.js +4 -0
- package/dist/chunks/format-host-5ja5gxyk.js +4 -0
- package/dist/chunks/fs-events-3424ezp3.js +4 -0
- package/dist/chunks/grid-open-1mpq9mzb.js +4 -0
- package/dist/chunks/{grid-view-tghcwrrt.js → grid-view-w9nfy203.js} +16 -16
- package/dist/chunks/icons-gb967qc8.js +4 -0
- package/dist/chunks/iframe-host-wsg0bn73.js +4 -0
- package/dist/chunks/image-bake-4afr4y6a.js +4 -0
- package/dist/chunks/image-editor-x7t8jbjn.js +4 -0
- package/dist/chunks/index-f9qr7kzh.js +4 -0
- package/dist/chunks/{index-vr006kbt.js → index-yjrfeks8.js} +3 -3
- package/dist/chunks/{javascript-z1g3s9fd.js → javascript-hy2mws8v.js} +3 -3
- package/dist/chunks/{jsonMode-7szwg8c4.js → jsonMode-vj30te2h.js} +6 -6
- package/dist/chunks/jx-validate-ttr5ewf0.js +4 -0
- package/dist/chunks/layers-m49nmwvx.js +4 -0
- package/dist/chunks/layers-panel-hvy2e549.js +4 -0
- package/dist/chunks/lit-html-k2mb9k0r.js +4 -0
- package/dist/chunks/media-library-ae67q4at.js +4 -0
- package/dist/chunks/media-open-yp9jm834.js +4 -0
- package/dist/chunks/media-picker-s41ngdfr.js +4 -0
- package/dist/chunks/{monaco-binding-v42w9c1v.js → monaco-binding-a51py33c.js} +3 -3
- package/dist/chunks/monaco-buffer-pa0g6ax7.js +4 -0
- package/dist/chunks/{monaco-setup-7bffxd6v.js → monaco-setup-g9kterhx.js} +3 -3
- package/dist/chunks/notify-bwehrp7q.js +4 -0
- package/dist/chunks/parse-hee7ne9b.js +4 -0
- package/dist/chunks/platform-kv0k65cy.js +4 -0
- package/dist/chunks/publish-panel-gzfxcm0t.js +4 -0
- package/dist/chunks/publish-quick-w3nxd7q4.js +4 -0
- package/dist/chunks/publish-workspace-dppd82tc.js +4 -0
- package/dist/chunks/quick-search-tdxaam55.js +4 -0
- package/dist/chunks/references-vqyr5s9x.js +4 -0
- package/dist/chunks/right-panel-0yrw4cgz.js +4 -0
- package/dist/chunks/source-control-6fk72me2.js +4 -0
- package/dist/chunks/{studio-j3d2fjqz.js → studio-0939t1q1.js} +4 -4
- package/dist/chunks/studio-0gvvqj3h.js +5 -0
- package/dist/chunks/studio-0khd8xtx.js +4 -0
- package/dist/chunks/studio-0w3541dg.js +7 -0
- package/dist/chunks/{studio-9b7177jq.js → studio-0x6f3nk9.js} +4 -4
- package/dist/chunks/studio-1410q5dw.js +5 -0
- package/dist/chunks/{studio-awxwjawq.js → studio-15hwqgd8.js} +4 -4
- package/dist/chunks/studio-1ks5d9y4.js +5 -0
- package/dist/chunks/{studio-881z1xrx.js → studio-2ggfzrrd.js} +4 -4
- package/dist/chunks/{studio-4jhw38b2.js → studio-321ym1tt.js} +4 -4
- package/dist/chunks/studio-3c10v2q1.js +5 -0
- package/dist/chunks/{studio-bvs9kpst.js → studio-3na1r48j.js} +8 -8
- package/dist/chunks/{studio-86r8hp7r.js → studio-3q7mhbgx.js} +5 -5
- package/dist/chunks/studio-3qr08sxq.js +4 -0
- package/dist/chunks/studio-3xk9zdgb.js +5 -0
- package/dist/chunks/{studio-qh8berdz.js → studio-423tfdte.js} +4 -4
- package/dist/chunks/{studio-75kcdbsk.js → studio-45mj4jqe.js} +4 -4
- package/dist/chunks/studio-4f7thq0p.js +5 -0
- package/dist/chunks/studio-4fnb5648.js +5 -0
- package/dist/chunks/studio-4zd6an2b.js +5 -0
- package/dist/chunks/studio-53wy9ek3.js +5 -0
- package/dist/chunks/studio-5n193zqd.js +5 -0
- package/dist/chunks/{studio-9rbe2xpw.js → studio-5tet77de.js} +3 -3
- package/dist/chunks/{studio-ybnwymxw.js → studio-7rrdbvex.js} +4 -4
- package/dist/chunks/studio-8kx6m0rg.js +5 -0
- package/dist/chunks/{studio-sxdb9h3q.js → studio-90zkgne5.js} +7 -7
- package/dist/chunks/studio-9c6cbq2c.js +4 -0
- package/dist/chunks/studio-9dq7rwc7.js +5 -0
- package/dist/chunks/{studio-cg1bfyhf.js → studio-9qx1ywbv.js} +8 -8
- package/dist/chunks/studio-a65kbs2e.js +9 -0
- package/dist/chunks/studio-aam8zt8z.js +5 -0
- package/dist/chunks/{studio-er5f383a.js → studio-abh9kfdh.js} +4 -4
- package/dist/chunks/studio-b4e0ftkt.js +5 -0
- package/dist/chunks/studio-b68k0z00.js +5 -0
- package/dist/chunks/{studio-399a25za.js → studio-bct64zmj.js} +4 -4
- package/dist/chunks/studio-bz0b5brj.js +6 -0
- package/dist/chunks/studio-cfnn70h1.js +5 -0
- package/dist/chunks/studio-daksw8kh.js +5 -0
- package/dist/chunks/{studio-r7pf0rkv.js → studio-e7vjyp04.js} +7 -7
- package/dist/chunks/studio-ea6r1zyr.js +5 -0
- package/dist/chunks/studio-egnjpyde.js +9 -0
- package/dist/chunks/studio-ejrypvmq.js +5 -0
- package/dist/chunks/studio-eqcddvc2.js +928 -0
- package/dist/chunks/studio-f8tyeyzt.js +5 -0
- package/dist/chunks/studio-fj6c905s.js +5 -0
- package/dist/chunks/{studio-646nn2ys.js → studio-fkrr7kgg.js} +5 -5
- package/dist/chunks/studio-fspe7m9t.js +5 -0
- package/dist/chunks/studio-g0hxtcga.js +5 -0
- package/dist/chunks/{studio-th7z3fg7.js → studio-g9brbewz.js} +4 -4
- package/dist/chunks/studio-ggx3j7nd.js +5 -0
- package/dist/chunks/{studio-43y04qck.js → studio-hfxttkyr.js} +4 -4
- package/dist/chunks/studio-hh394phe.js +5 -0
- package/dist/chunks/{studio-pjtntksy.js → studio-j0zf5x2x.js} +4 -4
- package/dist/chunks/studio-jeb7hth3.js +5 -0
- package/dist/chunks/studio-k0xzrse2.js +64 -0
- package/dist/chunks/studio-k1xqrgm5.js +5 -0
- package/dist/chunks/studio-k2evngrj.js +7 -0
- package/dist/chunks/studio-k3we3kqb.js +5 -0
- package/dist/chunks/{studio-sevbc3xv.js → studio-n9qba0gx.js} +4 -4
- package/dist/chunks/{studio-6hzdjfv0.js → studio-nf8esxxc.js} +4 -4
- package/dist/chunks/studio-nn4f9m9c.js +5 -0
- package/dist/chunks/studio-nskxe9q1.js +6 -0
- package/dist/chunks/{studio-ea74s8c1.js → studio-nvcn2ykc.js} +4 -4
- package/dist/chunks/studio-nwynxqtb.js +5 -0
- package/dist/chunks/studio-nzemfbkk.js +14 -0
- package/dist/chunks/studio-p2a03pgm.js +5 -0
- package/dist/chunks/{studio-4g8yb6jn.js → studio-pm17wdc4.js} +4 -4
- package/dist/chunks/{studio-71m92v9s.js → studio-q80fg1d6.js} +7 -7
- package/dist/chunks/studio-r050sez0.js +6 -0
- package/dist/chunks/studio-s7t4vp1d.js +6 -0
- package/dist/chunks/studio-s9fng7m8.js +7 -0
- package/dist/chunks/studio-sce30r7b.js +4 -0
- package/dist/chunks/studio-stahswgb.js +7 -0
- package/dist/chunks/studio-swedpd82.js +5 -0
- package/dist/chunks/studio-thz5t120.js +5 -0
- package/dist/chunks/studio-tpxcryf2.js +5 -0
- package/dist/chunks/{studio-wc6za3zs.js → studio-v5wktj6f.js} +4 -4
- package/dist/chunks/{studio-kg1abnz0.js → studio-vpevkfxw.js} +4 -4
- package/dist/chunks/studio-waekn290.js +5 -0
- package/dist/chunks/studio-wfct4h6n.js +5 -0
- package/dist/chunks/studio-wgc54sxf.js +5 -0
- package/dist/chunks/studio-wqfaqw4m.js +5 -0
- package/dist/chunks/studio-wse1rs8t.js +7 -0
- package/dist/chunks/studio-x0gsfr1d.js +16 -0
- package/dist/chunks/{studio-hhp4x8xf.js → studio-xpx91fby.js} +5 -5
- package/dist/chunks/studio-y1b86ast.js +5 -0
- package/dist/chunks/studio-yrcrncev.js +5 -0
- package/dist/chunks/{studio-rzqaz21g.js → studio-yzpskb0j.js} +4 -4
- package/dist/chunks/studio-zfdbmhp3.js +5 -0
- package/dist/chunks/{tsMode-f54j66ym.js → tsMode-jg1dtphj.js} +7 -7
- package/dist/chunks/workspace-9btkc8tk.js +4 -0
- package/dist/chunks/workspace-home-qcc4z5ag.js +4 -0
- package/dist/chunks/{ws-client-e8kj684q.js → ws-client-arz2qsy9.js} +3 -3
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +83 -78
- package/dist/studio.js +84 -84
- package/package.json +2 -2
- package/src/browse/library-pane.ts +44 -29
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +13 -4
- package/src/canvas/asset-resolve.ts +54 -11
- package/src/canvas/canvas-render.ts +25 -0
- package/src/canvas/iframe-entry.ts +5 -1
- package/src/canvas/iframe-host.ts +16 -6
- package/src/canvas/iframe-protocol.ts +9 -1
- package/src/commands/context.ts +5 -0
- package/src/commands/live-context.ts +5 -2
- package/src/editor/shortcuts.ts +3 -0
- package/src/files/asset-versions.ts +108 -2
- package/src/files/file-ops.ts +11 -1
- package/src/files/files.ts +111 -48
- package/src/files/media-paths.ts +12 -4
- package/src/files/media-upload.ts +195 -21
- package/src/files/new-file-name.ts +39 -1
- package/src/media/image-bake.ts +164 -0
- package/src/media/image-editor.ts +846 -0
- package/src/media/image-format.ts +262 -0
- package/src/media/image-recipe.ts +651 -0
- package/src/media/media-commands.ts +65 -1
- package/src/media/media-library-model.ts +1 -1
- package/src/media/media-library.ts +175 -15
- package/src/media/media-open.ts +156 -3
- package/src/media/media-pane.ts +8 -4
- package/src/panels/pane-context.ts +6 -1
- package/src/panels/tab-strip.ts +12 -3
- package/src/studio.ts +13 -2
- package/src/surfaces/image-editor.json +1390 -0
- package/src/surfaces/image-editor.ts +272 -0
- package/src/surfaces/media-library.json +129 -1
- package/src/surfaces/media-library.ts +17 -2
- package/src/types.ts +2 -1
- package/src/workspace/tab-workspace.ts +2 -1
- package/src/workspace/workspace-switch.ts +3 -1
- package/src/workspace/workspace.ts +22 -1
- package/dist/chunks/adapter-pp011m5x.js +0 -4
- package/dist/chunks/add-new-flow-ecqmzz76.js +0 -4
- package/dist/chunks/cf-account-picker-c07znkqt.js +0 -4
- package/dist/chunks/cf-settings-0v2bmqy0.js +0 -4
- package/dist/chunks/components-yn0dn2pq.js +0 -4
- package/dist/chunks/content-source-g0j41yy6.js +0 -4
- package/dist/chunks/convert-to-repeater-sbjk7tgn.js +0 -4
- package/dist/chunks/editor-xz02jeh1.js +0 -4
- package/dist/chunks/entry-commands-kmt02crp.js +0 -4
- package/dist/chunks/extension-sections-0rprf12d.js +0 -4
- package/dist/chunks/file-ops-mexj5qed.js +0 -4
- package/dist/chunks/files-b2tcppdd.js +0 -4
- package/dist/chunks/format-host-btsevv0c.js +0 -4
- package/dist/chunks/fs-events-r0apejva.js +0 -4
- package/dist/chunks/grid-open-n2a8xkht.js +0 -4
- package/dist/chunks/icons-853n6qt3.js +0 -4
- package/dist/chunks/iframe-host-gx0ypyh3.js +0 -4
- package/dist/chunks/index-bs4k9tr6.js +0 -4
- package/dist/chunks/jx-validate-ymf10teh.js +0 -4
- package/dist/chunks/layers-p9nfya38.js +0 -4
- package/dist/chunks/layers-panel-tt1hzhhf.js +0 -4
- package/dist/chunks/lit-html-x18n2sfz.js +0 -4
- package/dist/chunks/media-library-0n45gv8r.js +0 -4
- package/dist/chunks/media-picker-s09at95z.js +0 -4
- package/dist/chunks/monaco-buffer-een2ea0v.js +0 -4
- package/dist/chunks/notify-qwb8radt.js +0 -4
- package/dist/chunks/parse-v77xtwv7.js +0 -4
- package/dist/chunks/platform-8jnez0wr.js +0 -4
- package/dist/chunks/publish-panel-aysnj611.js +0 -4
- package/dist/chunks/publish-quick-kpbqs9mz.js +0 -4
- package/dist/chunks/publish-workspace-nd0a8q99.js +0 -4
- package/dist/chunks/quick-search-vg0jkhk2.js +0 -4
- package/dist/chunks/references-a7v6a0e2.js +0 -4
- package/dist/chunks/right-panel-cvk1vxx5.js +0 -4
- package/dist/chunks/source-control-fsqzc6he.js +0 -4
- package/dist/chunks/studio-05jfwbmf.js +0 -5
- package/dist/chunks/studio-0q5jwddh.js +0 -9
- package/dist/chunks/studio-0w2dr0q8.js +0 -5
- package/dist/chunks/studio-1tzpds97.js +0 -9
- package/dist/chunks/studio-216b27sv.js +0 -4
- package/dist/chunks/studio-2ejhqns4.js +0 -5
- package/dist/chunks/studio-2r90cts3.js +0 -16
- package/dist/chunks/studio-399xknkb.js +0 -6
- package/dist/chunks/studio-4ncmwqw5.js +0 -5
- package/dist/chunks/studio-5atwjgkj.js +0 -7
- package/dist/chunks/studio-5frqk7c0.js +0 -5
- package/dist/chunks/studio-6cwj6xrx.js +0 -5
- package/dist/chunks/studio-78pywnp2.js +0 -6
- package/dist/chunks/studio-886t6ztg.js +0 -5
- package/dist/chunks/studio-8wnvtr4z.js +0 -5
- package/dist/chunks/studio-8z66jk4z.js +0 -5
- package/dist/chunks/studio-b2qpt0xy.js +0 -14
- package/dist/chunks/studio-c71806xz.js +0 -7
- package/dist/chunks/studio-cbwks69x.js +0 -928
- package/dist/chunks/studio-cg710sk9.js +0 -5
- package/dist/chunks/studio-cw0wb9qk.js +0 -7
- package/dist/chunks/studio-cyheys7k.js +0 -5
- package/dist/chunks/studio-ea5m9kg5.js +0 -5
- package/dist/chunks/studio-efdxjwyp.js +0 -64
- package/dist/chunks/studio-egmw2837.js +0 -5
- package/dist/chunks/studio-f746039f.js +0 -5
- package/dist/chunks/studio-fmdymd9v.js +0 -5
- package/dist/chunks/studio-gcm12z00.js +0 -6
- package/dist/chunks/studio-gj98m8gy.js +0 -7
- package/dist/chunks/studio-hasbgjah.js +0 -5
- package/dist/chunks/studio-hz5n7x27.js +0 -5
- package/dist/chunks/studio-jeraxdgv.js +0 -7
- package/dist/chunks/studio-jvgek9xt.js +0 -5
- package/dist/chunks/studio-jws1a87n.js +0 -5
- package/dist/chunks/studio-k2sz13zg.js +0 -5
- package/dist/chunks/studio-k3m4jkdk.js +0 -5
- package/dist/chunks/studio-kg5ya5r2.js +0 -5
- package/dist/chunks/studio-khwbgrtg.js +0 -5
- package/dist/chunks/studio-mx6wzh25.js +0 -5
- package/dist/chunks/studio-mxs3ar9t.js +0 -5
- package/dist/chunks/studio-nm4szey4.js +0 -5
- package/dist/chunks/studio-nqxqf8p2.js +0 -4
- package/dist/chunks/studio-qn7z4krm.js +0 -5
- package/dist/chunks/studio-qyed0ah6.js +0 -5
- package/dist/chunks/studio-r4bcdwhq.js +0 -5
- package/dist/chunks/studio-rtfnhrdg.js +0 -5
- package/dist/chunks/studio-s5tzw84t.js +0 -5
- package/dist/chunks/studio-t51y8v1d.js +0 -5
- package/dist/chunks/studio-t98vq4r6.js +0 -5
- package/dist/chunks/studio-tsxpqvh0.js +0 -6
- package/dist/chunks/studio-v1yascv2.js +0 -5
- package/dist/chunks/studio-wxq6v5sv.js +0 -5
- package/dist/chunks/studio-y60sts62.js +0 -4
- package/dist/chunks/studio-zndrqawb.js +0 -5
- package/dist/chunks/studio-zwaxn5s2.js +0 -5
- package/dist/chunks/workspace-home-x1787wpv.js +0 -4
- package/dist/chunks/workspace-zpe1np43.js +0 -4
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* arrived, and what a script or the palette runs to ask "what is unused".
|
|
7
7
|
* - `media.upload` shows the Media home and opens its Upload picker.
|
|
8
8
|
* - `media.replace { path }` selects a file and opens its Replace picker: the one overwrite.
|
|
9
|
+
* - `media.rename { path }` renames a file, through the dialog that counts what will be rewritten.
|
|
10
|
+
* - `media.duplicate { path }` copies a file to a new name beside it, and selects the copy.
|
|
9
11
|
* - `media.delete { path }` deletes a file, through the confirmation that states its usage.
|
|
12
|
+
* - `media.edit { path }` opens an image in its Image editor (§9.5), the only way that mode is
|
|
13
|
+
* entered by a verb.
|
|
10
14
|
*
|
|
11
15
|
* Every one runs the flow, never a copy of it: the surface's Upload label and Replace input are the
|
|
12
16
|
* same controls these open (`HTMLInputElement.showPicker()`), and Delete is the detail pane's own.
|
|
@@ -31,13 +35,17 @@ import type { MediaBrowseRequest } from "./media-library";
|
|
|
31
35
|
import type { CommandArgValues } from "../commands/command-args";
|
|
32
36
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
33
37
|
import type * as MediaLibraryModule from "./media-library";
|
|
38
|
+
import type * as MediaOpenModule from "./media-open";
|
|
34
39
|
import type * as WorkspaceHomeModule from "../workspace/workspace-home";
|
|
35
40
|
|
|
36
41
|
/** The ids, which Add new and the Site panel run. */
|
|
37
42
|
export const MEDIA_BROWSE = "media.browse";
|
|
38
43
|
export const MEDIA_UPLOAD = "media.upload";
|
|
39
44
|
export const MEDIA_REPLACE = "media.replace";
|
|
45
|
+
export const MEDIA_RENAME = "media.rename";
|
|
46
|
+
export const MEDIA_DUPLICATE = "media.duplicate";
|
|
40
47
|
export const MEDIA_DELETE = "media.delete";
|
|
48
|
+
export const MEDIA_EDIT = "media.edit";
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
51
|
* The flow and the home opener, each imported once. One cached promise per module: two dynamic
|
|
@@ -45,6 +53,7 @@ export const MEDIA_DELETE = "media.delete";
|
|
|
45
53
|
*/
|
|
46
54
|
let _library: Promise<typeof MediaLibraryModule> | null = null;
|
|
47
55
|
let _home: Promise<typeof WorkspaceHomeModule> | null = null;
|
|
56
|
+
let _open: Promise<typeof MediaOpenModule> | null = null;
|
|
48
57
|
|
|
49
58
|
function library(): Promise<typeof MediaLibraryModule> {
|
|
50
59
|
_library ??= import("./media-library");
|
|
@@ -56,6 +65,11 @@ function home(): Promise<typeof WorkspaceHomeModule> {
|
|
|
56
65
|
return _home;
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
function opener(): Promise<typeof MediaOpenModule> {
|
|
69
|
+
_open ??= import("./media-open");
|
|
70
|
+
return _open;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
/** A path argument that must name a media file. Refused with the sentence a person can act on. */
|
|
60
74
|
function mediaPathArg(commandId: string, args: CommandArgValues, key: string): string {
|
|
61
75
|
const path = stringArg(commandId, args, key).replace(/^\.\//, "");
|
|
@@ -91,7 +105,7 @@ async function showHome(): Promise<void> {
|
|
|
91
105
|
|
|
92
106
|
const PATH_DESCRIPTION = "The media file's project path, such as public/media/hero.jpg.";
|
|
93
107
|
|
|
94
|
-
/** The
|
|
108
|
+
/** The seven records. */
|
|
95
109
|
export function mediaCommands(): AnyCommand[] {
|
|
96
110
|
const base = {
|
|
97
111
|
category: "Project",
|
|
@@ -154,6 +168,56 @@ export function mediaCommands(): AnyCommand[] {
|
|
|
154
168
|
title: "Replace Media File…",
|
|
155
169
|
undo: "none",
|
|
156
170
|
},
|
|
171
|
+
{
|
|
172
|
+
...base,
|
|
173
|
+
args: argsSchema(
|
|
174
|
+
{ path: stringProperty(`The image to edit: a PNG, JPEG or WebP. ${PATH_DESCRIPTION}`) },
|
|
175
|
+
["path"],
|
|
176
|
+
),
|
|
177
|
+
icon: "crop",
|
|
178
|
+
id: MEDIA_EDIT,
|
|
179
|
+
requires: "an open project, on a backend that can read a file's raw bytes",
|
|
180
|
+
run: async (_ctx, args) => {
|
|
181
|
+
const path = mediaPathArg(MEDIA_EDIT, args, "path");
|
|
182
|
+
const flow = await opener();
|
|
183
|
+
flow.openImageEditor(path);
|
|
184
|
+
},
|
|
185
|
+
title: "Edit Image…",
|
|
186
|
+
undo: "none",
|
|
187
|
+
/* Not gated on `collab.readOnly`: that is the FOCUSED tab's session, and this record takes a
|
|
188
|
+
path that may be another file. The refusal asks the path's own tab, in the flow. */
|
|
189
|
+
when: (ctx) => ctx.project.open && ctx.capability.readFileBytes,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
...base,
|
|
193
|
+
args: argsSchema({ path: stringProperty(`The file to rename. ${PATH_DESCRIPTION}`) }, [
|
|
194
|
+
"path",
|
|
195
|
+
]),
|
|
196
|
+
icon: "pencil-simple",
|
|
197
|
+
id: MEDIA_RENAME,
|
|
198
|
+
run: async (_ctx, args) => {
|
|
199
|
+
const path = mediaPathArg(MEDIA_RENAME, args, "path");
|
|
200
|
+
const flow = await library();
|
|
201
|
+
await flow.renameMediaFile(path);
|
|
202
|
+
},
|
|
203
|
+
title: "Rename Media File…",
|
|
204
|
+
undo: "none",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
...base,
|
|
208
|
+
args: argsSchema({ path: stringProperty(`The file to copy. ${PATH_DESCRIPTION}`) }, ["path"]),
|
|
209
|
+
icon: "copy",
|
|
210
|
+
id: MEDIA_DUPLICATE,
|
|
211
|
+
requires: "an open project, on a backend that can read a file's raw bytes",
|
|
212
|
+
run: async (_ctx, args) => {
|
|
213
|
+
const path = mediaPathArg(MEDIA_DUPLICATE, args, "path");
|
|
214
|
+
const flow = await library();
|
|
215
|
+
await flow.duplicateMediaFile(path);
|
|
216
|
+
},
|
|
217
|
+
title: "Duplicate Media File",
|
|
218
|
+
undo: "none",
|
|
219
|
+
when: (ctx) => ctx.project.open && ctx.capability.readFileBytes,
|
|
220
|
+
},
|
|
157
221
|
{
|
|
158
222
|
...base,
|
|
159
223
|
args: argsSchema({ path: stringProperty(`The file to delete. ${PATH_DESCRIPTION}`) }, [
|
|
@@ -97,7 +97,7 @@ export function passesFilter(
|
|
|
97
97
|
* says. Not its path: every path starts `public/` or `content/`, so a one-letter search for "b" or
|
|
98
98
|
* "c" kept the whole library and looked broken.
|
|
99
99
|
*/
|
|
100
|
-
export function matchesQuery(file: LibraryMediaFile, query: string): boolean {
|
|
100
|
+
export function matchesQuery(file: Pick<LibraryMediaFile, "name">, query: string): boolean {
|
|
101
101
|
const needle = query.trim().toLowerCase();
|
|
102
102
|
return needle === "" || file.name.toLowerCase().includes(needle);
|
|
103
103
|
}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* One flow, two modes (studio.md §22.6). In **manage** mode it is the Media home tab: filters on
|
|
6
6
|
* the left, a grid of tiles in the middle, and the selected file's detail on the right, with
|
|
7
|
-
* **Replace**, **Open in viewer** and **Delete**. In **pick** mode it is
|
|
8
|
-
* (§9.3), the same surface inside a `jx-dialog` whose footer answers
|
|
9
|
-
* compact popover's **Browse all media…** opens it
|
|
7
|
+
* **Replace**, **Open in viewer**, **Rename**, **Duplicate** and **Delete**. In **pick** mode it is
|
|
8
|
+
* the full-size media picker (§9.3), the same surface inside a `jx-dialog` whose footer answers
|
|
9
|
+
* Cancel or Use <file>; the compact popover's **Browse all media…** opens it
|
|
10
|
+
* (`ui/media-picker.ts`).
|
|
10
11
|
*
|
|
11
12
|
* **What it lists is `ui/media-picker.ts`'s listing**, `public/**` and every collection's
|
|
12
13
|
* `images/**`, so the workspace, the pickers and the Site panel cannot disagree about what exists.
|
|
@@ -27,8 +28,10 @@
|
|
|
27
28
|
* drop, only files the listing will show are uploaded, and the first upload is selected at once, so
|
|
28
29
|
* the detail pane describes it, and its tile scrolls into view, the moment the re-read listing has
|
|
29
30
|
* it. **Replace** is the one overwrite (`replaceAsset`), offered only files of the replaced one's
|
|
30
|
-
* own type; **
|
|
31
|
-
*
|
|
31
|
+
* own type; **Rename** keeps the file's folder and format and refuses a name already taken, and
|
|
32
|
+
* **Duplicate** copies the bytes on disk to a new name beside it (both studio.md §9.3); **Delete**
|
|
33
|
+
* goes through the same confirmation as every delete, which states the usage line
|
|
34
|
+
* (`files/file-ops.ts`), and a delete that empties an upload destination (`public/media/`, a
|
|
32
35
|
* collection's `images/`) takes the folder the upload created with it.
|
|
33
36
|
*
|
|
34
37
|
* **This module is the flow; `surfaces/media-library.json` is the markup.** Every decision is made
|
|
@@ -42,14 +45,17 @@ import { getPlatform } from "../platform";
|
|
|
42
45
|
import { notify } from "../services/notify";
|
|
43
46
|
import { invalidateUsages, onUsagesInvalidated, usagesSupported } from "../services/references";
|
|
44
47
|
import { overlayRegion, paneRegion } from "../ui/regions";
|
|
45
|
-
import { layerHost } from "../ui/layers";
|
|
48
|
+
import { layerHost, showPromptDialog } from "../ui/layers";
|
|
46
49
|
import { rectOf } from "../utils/geometry";
|
|
47
50
|
import { activeTab, closeTab, workspace } from "../workspace/workspace";
|
|
48
51
|
import { createPreviewObserver } from "../browse/library-preview";
|
|
49
52
|
import {
|
|
50
53
|
DEFAULT_MEDIA_DIR,
|
|
51
54
|
announceMediaChanged,
|
|
55
|
+
duplicateAsset,
|
|
52
56
|
isUploadDestination,
|
|
57
|
+
joinDir,
|
|
58
|
+
mediaRenameRefusal,
|
|
53
59
|
replaceAccept,
|
|
54
60
|
replaceAsset,
|
|
55
61
|
uploadAccept,
|
|
@@ -63,12 +69,19 @@ import {
|
|
|
63
69
|
peekMediaMeta,
|
|
64
70
|
recordImageSize,
|
|
65
71
|
} from "../files/media-meta";
|
|
66
|
-
import { dirName, previewFileSrc } from "../files/media-paths";
|
|
72
|
+
import { baseName, dirName, previewFileSrc } from "../files/media-paths";
|
|
67
73
|
import { forgetVersions } from "../files/asset-versions";
|
|
68
74
|
import { loadMediaUsages, peekMediaUsages } from "../files/media-usage";
|
|
69
75
|
import { onProjectMediaChange, projectMediaFiles } from "../ui/media-picker";
|
|
70
76
|
import { openMenu } from "../surfaces/menu";
|
|
71
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
canDuplicateMedia,
|
|
79
|
+
canEditImage,
|
|
80
|
+
mediaWriteRefused,
|
|
81
|
+
openImageEditor,
|
|
82
|
+
openMediaTab,
|
|
83
|
+
} from "./media-open";
|
|
84
|
+
import { imageEditorAllowsReplace, imageFileChanged } from "./image-editor";
|
|
72
85
|
import { factsLine, mediaStageFor, specimenOf, usageOf } from "./media-pane";
|
|
73
86
|
import {
|
|
74
87
|
MEDIA_FILTERS,
|
|
@@ -247,6 +260,11 @@ interface LibraryMount {
|
|
|
247
260
|
* hold yet when it is selected, and which a sort can place anywhere in the grid.
|
|
248
261
|
*/
|
|
249
262
|
reveal: string | null;
|
|
263
|
+
/**
|
|
264
|
+
* Whether {@link reveal}'s tile also takes the keyboard. A rename and a duplicate draw a NEW tile
|
|
265
|
+
* for the file the reader was on, and the focus they left on the old one is gone with it.
|
|
266
|
+
*/
|
|
267
|
+
revealFocus: boolean;
|
|
250
268
|
/**
|
|
251
269
|
* Whether the keyboard is owed to this library's tab stop ({@link focusMediaHome}) and no tile
|
|
252
270
|
* has been drawn to take it yet: a home mounted before its listing has arrived has no tiles.
|
|
@@ -319,6 +337,10 @@ function revealPending(mount: LibraryMount): void {
|
|
|
319
337
|
if (tile?.isConnected) {
|
|
320
338
|
mount.reveal = null;
|
|
321
339
|
tile.scrollIntoView({ block: "nearest" });
|
|
340
|
+
if (mount.revealFocus) {
|
|
341
|
+
mount.revealFocus = false;
|
|
342
|
+
tile.focus();
|
|
343
|
+
}
|
|
322
344
|
}
|
|
323
345
|
}
|
|
324
346
|
|
|
@@ -578,6 +600,8 @@ function detailView(
|
|
|
578
600
|
| "previewSpecimen"
|
|
579
601
|
| "previewSpecimenFamily"
|
|
580
602
|
| "replaceAccept"
|
|
603
|
+
| "editable"
|
|
604
|
+
| "duplicable"
|
|
581
605
|
| "usageState"
|
|
582
606
|
| "usageHeadline"
|
|
583
607
|
| "usageFiles"
|
|
@@ -597,6 +621,8 @@ function detailView(
|
|
|
597
621
|
previewSpecimenFamily: "",
|
|
598
622
|
previewSrc: "",
|
|
599
623
|
replaceAccept: "",
|
|
624
|
+
editable: "no",
|
|
625
|
+
duplicable: "no",
|
|
600
626
|
selectedFile: "",
|
|
601
627
|
usageFiles: [],
|
|
602
628
|
usageHeadline: "",
|
|
@@ -638,6 +664,8 @@ function detailView(
|
|
|
638
664
|
previewSpecimenFamily: specimen.specimenFamily,
|
|
639
665
|
previewSrc: previewKind === "glyph" ? "" : previewFileSrc(entry.file),
|
|
640
666
|
replaceAccept: replaceAccept(entry.file),
|
|
667
|
+
editable: canEditImage(entry.file) ? "yes" : "no",
|
|
668
|
+
duplicable: canDuplicateMedia() ? "yes" : "no",
|
|
641
669
|
selectedFile: entry.file,
|
|
642
670
|
...usageOf(usage),
|
|
643
671
|
};
|
|
@@ -778,6 +806,99 @@ async function pruneEmptiedDestination(
|
|
|
778
806
|
}
|
|
779
807
|
}
|
|
780
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Rename a media file, in the dialog every rename goes through (studio.md §9.3): the references the
|
|
811
|
+
* refactor pass will rewrite are counted before the field opens, and the field refuses a name the
|
|
812
|
+
* file cannot take, a taken name above all, because the backend would overwrite it in silence.
|
|
813
|
+
*
|
|
814
|
+
* Whatever showed the file goes on showing it: the selection follows it, and an open tab keeps its
|
|
815
|
+
* state under the new path.
|
|
816
|
+
*
|
|
817
|
+
* @returns The new path, or `null` when nothing was renamed.
|
|
818
|
+
*/
|
|
819
|
+
export async function renameMediaFile(path: string): Promise<string | null> {
|
|
820
|
+
if (mediaWriteRefused(path, "renamed")) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const name = baseName(path);
|
|
824
|
+
const dir = dirName(path);
|
|
825
|
+
const { commitRename, listRenameSiblings, notifyMoveOutcome, renameStatus } = await filesModule();
|
|
826
|
+
const taken = await listRenameSiblings(path, "Media");
|
|
827
|
+
if (taken === null) {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
const { renamePromptMessage } = await fileOpsModule();
|
|
831
|
+
const message = await renamePromptMessage(path);
|
|
832
|
+
const entered = await showPromptDialog("Rename", {
|
|
833
|
+
confirmLabel: "Rename",
|
|
834
|
+
select: "stem",
|
|
835
|
+
validate: (value) => mediaRenameRefusal(path, value, taken),
|
|
836
|
+
value: name,
|
|
837
|
+
...(message !== undefined && { message }),
|
|
838
|
+
});
|
|
839
|
+
if (!entered || entered === name) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const to = joinDir(dir, entered);
|
|
843
|
+
const measured = peekMediaMeta(path);
|
|
844
|
+
try {
|
|
845
|
+
const report = await commitRename(path, to);
|
|
846
|
+
if (homeChoice.selected === path) {
|
|
847
|
+
landOnNewFile(to);
|
|
848
|
+
}
|
|
849
|
+
await announceMediaChanged(dir);
|
|
850
|
+
/* The announcement drops every measurement, and a viewer already showing the picture will not
|
|
851
|
+
load it again to measure it. A rename changes no bytes, so what was true of them still is. */
|
|
852
|
+
if (measured?.width != null && measured.height != null) {
|
|
853
|
+
recordImageSize(to, measured.width, measured.height);
|
|
854
|
+
}
|
|
855
|
+
notifyMoveOutcome(renameStatus(entered, report), report, to);
|
|
856
|
+
return to;
|
|
857
|
+
} catch (error) {
|
|
858
|
+
notify.error(`Could not rename ${name}.`, {
|
|
859
|
+
detail: errorMessage(error),
|
|
860
|
+
path,
|
|
861
|
+
source: "Media",
|
|
862
|
+
});
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Duplicate a media file (studio.md §9.3), and select the copy in the Media home, as an upload is.
|
|
869
|
+
*
|
|
870
|
+
* @returns The copy's path, or `null` when none was made.
|
|
871
|
+
*/
|
|
872
|
+
export async function duplicateMediaFile(path: string): Promise<string | null> {
|
|
873
|
+
if (mediaWriteRefused(path, "duplicated")) {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
const copy = await duplicateAsset(path);
|
|
877
|
+
if (copy === null) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
landOnNewFile(copy.path);
|
|
881
|
+
redrawAll();
|
|
882
|
+
return copy.path;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Put the Media home's selection on a file a rename or a duplicate has just made: on screen, and
|
|
887
|
+
* holding the keyboard once its tile is drawn. Neither verb changes a file's kind, and a file just
|
|
888
|
+
* renamed or copied is as unused as it was, so the filter still keeps it; only a search the new
|
|
889
|
+
* name does not match has to go.
|
|
890
|
+
*/
|
|
891
|
+
function landOnNewFile(file: string): void {
|
|
892
|
+
homeChoice.selected = file;
|
|
893
|
+
if (!matchesQuery({ name: baseName(file) }, homeChoice.query)) {
|
|
894
|
+
homeChoice.query = "";
|
|
895
|
+
}
|
|
896
|
+
for (const { mount } of _panes.values()) {
|
|
897
|
+
mount.reveal = file;
|
|
898
|
+
mount.revealFocus = true;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
781
902
|
function actionsFor(mount: LibraryMount) {
|
|
782
903
|
return {
|
|
783
904
|
activate: (file: string) => {
|
|
@@ -857,22 +978,45 @@ function actionsFor(mount: LibraryMount) {
|
|
|
857
978
|
openMediaTab(selected);
|
|
858
979
|
}
|
|
859
980
|
},
|
|
981
|
+
editImage: () => {
|
|
982
|
+
const { selected } = mount.choice;
|
|
983
|
+
if (selected !== null) {
|
|
984
|
+
openImageEditor(selected);
|
|
985
|
+
}
|
|
986
|
+
},
|
|
860
987
|
remove: () => {
|
|
861
988
|
const { selected } = mount.choice;
|
|
862
989
|
if (selected !== null) {
|
|
863
990
|
void deleteMediaFile(selected);
|
|
864
991
|
}
|
|
865
992
|
},
|
|
993
|
+
rename: () => {
|
|
994
|
+
const { selected } = mount.choice;
|
|
995
|
+
if (selected !== null) {
|
|
996
|
+
void renameMediaFile(selected);
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
duplicate: () => {
|
|
1000
|
+
const { selected } = mount.choice;
|
|
1001
|
+
if (selected !== null) {
|
|
1002
|
+
void duplicateMediaFile(selected);
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
866
1005
|
replace: (input: HTMLInputElement) => {
|
|
867
1006
|
const [chosen] = input.files ?? [];
|
|
868
1007
|
// Cleared first: a file input that keeps its value refuses the same file twice.
|
|
869
1008
|
input.value = "";
|
|
870
1009
|
const { selected } = mount.choice;
|
|
871
|
-
if (chosen && selected !== null) {
|
|
1010
|
+
if (chosen && selected !== null && imageEditorAllowsReplace(selected)) {
|
|
872
1011
|
/* No metadata invalidation here: `replaceAsset` announces the change, whose handler drops
|
|
873
1012
|
the listing AND its metadata and re-reads them in that order. A second drop after it
|
|
874
1013
|
lands would wipe the fresh facts of every other tile, and nothing would re-seed them. */
|
|
875
|
-
void replaceAsset(selected, chosen).then(
|
|
1014
|
+
void replaceAsset(selected, chosen).then((replaced) => {
|
|
1015
|
+
if (replaced) {
|
|
1016
|
+
imageFileChanged(selected);
|
|
1017
|
+
}
|
|
1018
|
+
redrawAll();
|
|
1019
|
+
});
|
|
876
1020
|
}
|
|
877
1021
|
},
|
|
878
1022
|
reset: () => {
|
|
@@ -932,10 +1076,9 @@ let _tileMenu: MenuHandle | null = null;
|
|
|
932
1076
|
/**
|
|
933
1077
|
* A right-click on a tile: select it, then offer the detail pane's actions at the pointer.
|
|
934
1078
|
*
|
|
935
|
-
* The same
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
* Content workspace.
|
|
1079
|
+
* The same verbs as the detail pane's (studio.md §22.6), and each calls what the pane's control
|
|
1080
|
+
* calls, so the menu cannot drift from the pane. Selecting first is what makes the pane describe
|
|
1081
|
+
* the file the menu acts on, as a right-click on a row does in the Content workspace.
|
|
939
1082
|
*/
|
|
940
1083
|
function showTileMenu(mount: LibraryMount, file: string, x: number, y: number): void {
|
|
941
1084
|
select(mount, file);
|
|
@@ -969,8 +1112,24 @@ function showTileMenu(mount: LibraryMount, file: string, x: number, y: number):
|
|
|
969
1112
|
region: "media-tile",
|
|
970
1113
|
rows: [
|
|
971
1114
|
row("open", "Open in Viewer", () => openMediaTab(file)),
|
|
1115
|
+
...(canEditImage(file) ? [row("edit", "Edit Image", () => openImageEditor(file))] : []),
|
|
972
1116
|
row("copy-url", "Copy Site URL", () => copySiteUrl(entry)),
|
|
973
|
-
row(
|
|
1117
|
+
row(
|
|
1118
|
+
"rename",
|
|
1119
|
+
"Rename…",
|
|
1120
|
+
() => {
|
|
1121
|
+
void renameMediaFile(file);
|
|
1122
|
+
},
|
|
1123
|
+
{ dividerAbove: true },
|
|
1124
|
+
),
|
|
1125
|
+
...(canDuplicateMedia()
|
|
1126
|
+
? [
|
|
1127
|
+
row("duplicate", "Duplicate", () => {
|
|
1128
|
+
void duplicateMediaFile(file);
|
|
1129
|
+
}),
|
|
1130
|
+
]
|
|
1131
|
+
: []),
|
|
1132
|
+
row("replace", "Replace…", () => requestMediaReplace(file)),
|
|
974
1133
|
row(
|
|
975
1134
|
"delete",
|
|
976
1135
|
"Delete",
|
|
@@ -1055,6 +1214,7 @@ function createMount(
|
|
|
1055
1214
|
onSelect: null,
|
|
1056
1215
|
paneId,
|
|
1057
1216
|
reveal: null,
|
|
1217
|
+
revealFocus: false,
|
|
1058
1218
|
focusOwed: false,
|
|
1059
1219
|
region,
|
|
1060
1220
|
sweepTimer: null,
|
package/src/media/media-open.ts
CHANGED
|
@@ -18,9 +18,16 @@
|
|
|
18
18
|
* @docs studio/projects/media
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
21
22
|
import { activateTab, openTab, workspace } from "../workspace/workspace";
|
|
22
23
|
import { extensionOf, isMediaFile } from "../files/media-upload";
|
|
24
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
25
|
+
import { notify } from "../services/notify";
|
|
26
|
+
import { collabReadOnly } from "../collab/collab-session";
|
|
27
|
+
import { toRaw } from "../reactivity";
|
|
23
28
|
import { MEDIA_MODE } from "./media-pane";
|
|
29
|
+
import { IMAGE_MODE } from "./image-editor";
|
|
30
|
+
import { editRefusal, isEditableExtension } from "./image-format";
|
|
24
31
|
import type { Tab } from "../tabs/tab";
|
|
25
32
|
|
|
26
33
|
/** Placeholder document for a media tab — the viewer never reads it, and there is nothing to save. */
|
|
@@ -31,9 +38,21 @@ export function isViewableMedia(path: string): boolean {
|
|
|
31
38
|
return isMediaFile(path);
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* The modes a media tab offers, as far as the path can say. SVG is also text, so it keeps a source
|
|
43
|
+
* alternate; a still raster in a format the host can encode gets the Image editor (studio.md §9.5)
|
|
44
|
+
* on a host that can hand Studio a file's raw bytes. A refused format is not offered rather than
|
|
45
|
+
* offered and then refused, and the half of that the extension cannot answer (an animated `.png` or
|
|
46
|
+
* `.webp`, say) is {@link withdrawRefusedEditor}'s.
|
|
47
|
+
*/
|
|
35
48
|
export function mediaTabModes(path: string): string[] {
|
|
36
|
-
|
|
49
|
+
if (extensionOf(path) === ".svg") {
|
|
50
|
+
return [MEDIA_MODE, "source"];
|
|
51
|
+
}
|
|
52
|
+
if (isEditableExtension(path) && hasPlatform() && isFunction(getPlatform().readFileBytes)) {
|
|
53
|
+
return [MEDIA_MODE, IMAGE_MODE];
|
|
54
|
+
}
|
|
55
|
+
return [MEDIA_MODE];
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
/**
|
|
@@ -57,7 +76,7 @@ export function openMediaTab(
|
|
|
57
76
|
activateTab(path, { focus: opts.focus !== false });
|
|
58
77
|
return existing;
|
|
59
78
|
}
|
|
60
|
-
|
|
79
|
+
const tab = openTab({
|
|
61
80
|
capabilities: { modes: mediaTabModes(path) },
|
|
62
81
|
document: structuredClone(MEDIA_STUB_DOCUMENT),
|
|
63
82
|
documentPath: path,
|
|
@@ -67,4 +86,138 @@ export function openMediaTab(
|
|
|
67
86
|
...(opts.preview === true && { preview: true }),
|
|
68
87
|
...(opts.focus === false && { focus: false }),
|
|
69
88
|
});
|
|
89
|
+
void withdrawRefusedEditor(tab, path);
|
|
90
|
+
return tab;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Why a tab's Image editor was withdrawn, so a later attempt can say so again. */
|
|
94
|
+
const _withdrawn = new WeakMap<Tab, string>();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Take the Image editor off a tab whose bytes refuse it: an extension is a claim about the bytes,
|
|
98
|
+
* and a still `.png` and an animated one share an extension (studio.md §9.5).
|
|
99
|
+
*
|
|
100
|
+
* The extension already answered for every file the mode could never write, so this reads only a
|
|
101
|
+
* file the path says is editable, and the viewer is drawing those same bytes anyway. A read that
|
|
102
|
+
* fails decides nothing: the editor reports it if the author goes there. A tab already in the mode
|
|
103
|
+
* goes back to the viewer and says why, because a mode that vanished in silence would look like a
|
|
104
|
+
* click that did nothing.
|
|
105
|
+
*/
|
|
106
|
+
async function withdrawRefusedEditor(tab: Tab, path: string): Promise<void> {
|
|
107
|
+
if (!tab.capabilities.modes.includes(IMAGE_MODE)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let bytes: Uint8Array;
|
|
111
|
+
try {
|
|
112
|
+
bytes = new Uint8Array(await getPlatform().readFileBytes!(path));
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const refusal = editRefusal(path, bytes);
|
|
117
|
+
/* Through the workspace's own face of the tab: `openTab` hands back the raw object, and a write to
|
|
118
|
+
that changes the value without telling the Editor control. It also answers a tab the read
|
|
119
|
+
outlived, which has nothing left to withdraw from. */
|
|
120
|
+
const live = workspace.tabs.get(path);
|
|
121
|
+
if (refusal === null || !live) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
_withdrawn.set(toRaw(live), refusal);
|
|
125
|
+
live.capabilities.modes = live.capabilities.modes.filter((mode) => mode !== IMAGE_MODE);
|
|
126
|
+
if (live.session.ui.canvasMode === IMAGE_MODE) {
|
|
127
|
+
live.session.ui.canvasMode = MEDIA_MODE;
|
|
128
|
+
notify.warn(refusal, { key: `image.refused:${path}`, path, source: "Image" });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether to offer the Image editor for a file in a list of them: what the path says, and what its
|
|
134
|
+
* open tab has since learned. The palette's `media.edit` cannot ask this (its context describes the
|
|
135
|
+
* focused tab), so it is the flow's refusal there.
|
|
136
|
+
*/
|
|
137
|
+
export function canEditImage(path: string): boolean {
|
|
138
|
+
if (!mediaTabModes(path).includes(IMAGE_MODE)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const tab = workspace.tabs.get(path);
|
|
142
|
+
return !tab || (tab.capabilities.modes.includes(IMAGE_MODE) && !collabReadOnly(tab));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether this host can copy a media file: a copy needs its raw bytes (studio.md §9.3), and a
|
|
147
|
+
* backend that cannot read them is not offered Duplicate rather than offered it and then refused.
|
|
148
|
+
*/
|
|
149
|
+
export function canDuplicateMedia(): boolean {
|
|
150
|
+
return hasPlatform() && isFunction(getPlatform().readFileBytes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Whether a collaborator with read access to this file's session is the one asking to write it, and
|
|
155
|
+
* if so say so. Asks the PATH's own tab, as {@link openImageEditor} does: a record's context
|
|
156
|
+
* describes the focused tab, which may be another file.
|
|
157
|
+
*
|
|
158
|
+
* @param path The file.
|
|
159
|
+
* @param done How the sentence ends: "renamed", "duplicated".
|
|
160
|
+
*/
|
|
161
|
+
export function mediaWriteRefused(path: string, done: string): boolean {
|
|
162
|
+
const tab = workspace.tabs.get(path);
|
|
163
|
+
if (!tab || !collabReadOnly(tab)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const name = path.split("/").pop() ?? path;
|
|
167
|
+
notify.warn(`You have read access to this session, so ${name} cannot be ${done}.`, {
|
|
168
|
+
key: `media.readOnly:${path}`,
|
|
169
|
+
path,
|
|
170
|
+
source: "Collaboration",
|
|
171
|
+
});
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Open a media file in its Image editor (studio.md §9.5) — `media.edit`, and the Media workspace's
|
|
177
|
+
* Edit action. The mode is entered by choosing a subject, never by name through `canvas.setMode`.
|
|
178
|
+
*
|
|
179
|
+
* Refused, with the reason, for a file the editor cannot write (the tab would not offer the mode)
|
|
180
|
+
* and for a collaborator who cannot write the file: the gate asks the PATH's own tab, which is why
|
|
181
|
+
* it lives here rather than on the command record, whose context describes the focused tab.
|
|
182
|
+
*
|
|
183
|
+
* @returns The tab, now in the Image editor, or null when it was refused.
|
|
184
|
+
*/
|
|
185
|
+
export function openImageEditor(
|
|
186
|
+
path: string,
|
|
187
|
+
opts: { paneId?: string; focus?: boolean } = {},
|
|
188
|
+
): Tab | null {
|
|
189
|
+
const name = path.split("/").pop() ?? path;
|
|
190
|
+
if (!mediaTabModes(path).includes(IMAGE_MODE)) {
|
|
191
|
+
notify.warn(`${name} cannot be edited here.`, {
|
|
192
|
+
detail:
|
|
193
|
+
"The Image editor writes PNG, JPEG and WebP, on a backend that can hand Studio a file's raw bytes.",
|
|
194
|
+
key: `image.notEditable:${path}`,
|
|
195
|
+
path,
|
|
196
|
+
source: "Image",
|
|
197
|
+
});
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
openMediaTab(path, opts);
|
|
201
|
+
// Through the workspace's face of the tab, so the pane redraws (see withdrawRefusedEditor).
|
|
202
|
+
const tab = workspace.tabs.get(path)!;
|
|
203
|
+
/* Asked of the tab as it stands, after the open: a session may have attached in it, and bytes read
|
|
204
|
+
earlier may have withdrawn the mode. Either leaves the tab in its viewer, saying why. */
|
|
205
|
+
if (collabReadOnly(tab)) {
|
|
206
|
+
notify.warn(`You have read access to this session, so ${name} cannot be edited.`, {
|
|
207
|
+
key: `image.readOnly:${path}`,
|
|
208
|
+
path,
|
|
209
|
+
source: "Collaboration",
|
|
210
|
+
});
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (!tab.capabilities.modes.includes(IMAGE_MODE)) {
|
|
214
|
+
notify.warn(_withdrawn.get(toRaw(tab)) ?? `${name} cannot be edited here.`, {
|
|
215
|
+
key: `image.refused:${path}`,
|
|
216
|
+
path,
|
|
217
|
+
source: "Image",
|
|
218
|
+
});
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
tab.session.ui.canvasMode = IMAGE_MODE;
|
|
222
|
+
return tab;
|
|
70
223
|
}
|
package/src/media/media-pane.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* confirmation_: you could only learn what an image was used for by trying to remove it. This is
|
|
17
17
|
* the surface that query was waiting for.
|
|
18
18
|
*
|
|
19
|
-
* Read-only, deliberately. Rename,
|
|
20
|
-
* buttons for them here would be a second place to keep them
|
|
19
|
+
* Read-only, deliberately. Rename, duplicate, replace and delete are the Media home's and reveal is
|
|
20
|
+
* the file tree's, and a second set of buttons for them here would be a second place to keep them
|
|
21
|
+
* right.
|
|
21
22
|
*
|
|
22
23
|
* **This module is the flow; `surfaces/media-pane.json` is the markup.** Everything below decides
|
|
23
24
|
* something — which pane holds which tab, which of the six stages a file belongs in, when the
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
import { effect, effectScope } from "../reactivity";
|
|
31
32
|
import { paneRegion } from "../ui/regions";
|
|
32
33
|
import { localDay } from "../utils/local-day";
|
|
34
|
+
import { assetVersionsEpoch } from "../files/asset-versions";
|
|
33
35
|
import { mediaSiteUrl, previewFileSrc } from "../files/media-paths";
|
|
34
36
|
import {
|
|
35
37
|
formatBytes,
|
|
@@ -208,7 +210,6 @@ export function renderMediaMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
208
210
|
detachMediaPane(paneId);
|
|
209
211
|
|
|
210
212
|
const path = tab.documentPath ?? "";
|
|
211
|
-
const src = previewFileSrc(path);
|
|
212
213
|
const ref = mediaSiteUrl(path);
|
|
213
214
|
const ext = extensionOf(path);
|
|
214
215
|
const kind = mediaStageFor(path);
|
|
@@ -244,7 +245,9 @@ export function renderMediaMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
244
245
|
region: paneRegion(paneId, "media"),
|
|
245
246
|
specimen: specimen.specimen,
|
|
246
247
|
specimenFamily: specimen.specimenFamily,
|
|
247
|
-
|
|
248
|
+
/* Asked on every paint: bytes written in place give the URL a new repaint key (§3.4), and a
|
|
249
|
+
viewer holding the old URL would go on showing the picture they replaced. */
|
|
250
|
+
src: previewFileSrc(path),
|
|
248
251
|
...usageOf(usage),
|
|
249
252
|
};
|
|
250
253
|
};
|
|
@@ -285,6 +288,7 @@ export function renderMediaMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
285
288
|
browser's problem, and its metadata repaints through `redraw`. Reading it here is what
|
|
286
289
|
makes a rename repaint the viewer rather than leave it pointing at a name that is gone. */
|
|
287
290
|
void tab.documentPath;
|
|
291
|
+
void assetVersionsEpoch.value;
|
|
288
292
|
draw();
|
|
289
293
|
});
|
|
290
294
|
});
|
|
@@ -800,7 +800,12 @@ function editorAxis(tab: Tab): Partial<PaneContextView> {
|
|
|
800
800
|
const current = editorKindOf(tab);
|
|
801
801
|
const kinds = IMPLICIT_EDITOR_KINDS.has(current)
|
|
802
802
|
? [current]
|
|
803
|
-
: editorKindsOf(tab).filter(
|
|
803
|
+
: editorKindsOf(tab).filter(
|
|
804
|
+
(kind) =>
|
|
805
|
+
!IMPLICIT_EDITOR_KINDS.has(kind) &&
|
|
806
|
+
// A collaborator who cannot write is not offered the editor that exists to write (§9.5).
|
|
807
|
+
(kind !== "image" || kind === current || !collabState(tab).readOnly),
|
|
808
|
+
);
|
|
804
809
|
if (kinds.length < 2) {
|
|
805
810
|
// One kind is not a choice. Rendering it as a segmented group would be a control that cannot move —
|
|
806
811
|
// Which is the defect this axis exists to remove, in miniature.
|