Skip to content

Commit 96affb2

Browse files
committed
#85 - Support for PowerPoint slide images
1 parent 420b440 commit 96affb2

File tree

13 files changed

+220
-27
lines changed

13 files changed

+220
-27
lines changed

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,12 @@
341341
"title": "Export slides to PDF",
342342
"category": "Demo Time",
343343
"icon": "$(file-pdf)"
344+
},
345+
{
346+
"command": "demo-time.importPowerPointImages",
347+
"title": "Import PowerPoint Images as Slides",
348+
"category": "Demo Time",
349+
"icon": "$(file-media)"
344350
}
345351
],
346352
"menus": {
@@ -479,6 +485,11 @@
479485
{
480486
"command": "demo-time.addStepToDemo",
481487
"when": "(editorLangId == jsonc || editorLangId == json) && resourceDirname =~ /\\.demo$/",
488+
"group": "navigation@-1"
489+
},
490+
{
491+
"command": "demo-time.start",
492+
"when": "(editorLangId == jsonc || editorLangId == json) && resourceDirname =~ /\\.demo$/",
482493
"group": "navigation@0"
483494
},
484495
{

src/constants/Command.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ export const COMMAND = {
4242
togglePresentationView: `${EXTENSION_NAME}.togglePresentationView`,
4343
closePresentationView: `${EXTENSION_NAME}.closePresentationView`,
4444
exportToPdf: `${EXTENSION_NAME}.exportToPdf`,
45+
// Importer
46+
importPowerPointImages: `${EXTENSION_NAME}.importPowerPointImages`,
4547
};

src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ import {
99
DemoStatusBar,
1010
Extension,
1111
FileProvider,
12+
ImportService,
1213
NotesService,
14+
PdfExportService,
1315
Slides,
1416
UriHandler,
1517
} from "./services";
1618
import { DemoPanel } from "./panels/DemoPanel";
1719
import { Preview } from "./preview/Preview";
1820
import { PresenterView } from "./presenterView/PresenterView";
1921
import { Config } from "./constants";
20-
import { PdfExportService } from "./services/PdfExportService";
2122

2223
export async function activate(context: vscode.ExtensionContext) {
2324
Extension.getInstance(context);
@@ -40,6 +41,7 @@ export async function activate(context: vscode.ExtensionContext) {
4041
DemoApi.register();
4142
UriHandler.register();
4243
PdfExportService.register();
44+
ImportService.register();
4345

4446
console.log(`${Config.title} is active!`);
4547
}

src/services/DemoCreator.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { QuickPickItem, QuickPickItemKind, Uri, commands, window, workspace } from "vscode";
22
import { COMMAND, Config } from "../constants";
3-
import { Action, Demo, Demos, Step, Subscription } from "../models";
3+
import { Action, Demo, Demos, Icons, Step, Subscription } from "../models";
44
import { Extension } from "./Extension";
55
import { FileProvider } from "./FileProvider";
66
import { DemoPanel } from "../panels/DemoPanel";
@@ -254,7 +254,8 @@ export class DemoCreator {
254254
demo: Demos,
255255
step: Step | Step[],
256256
stepTitle?: string,
257-
stepDescription?: string
257+
stepDescription?: string,
258+
stepIcons?: Icons
258259
): Promise<Demo[] | undefined> {
259260
let demoStep: string | undefined = "New demo step";
260261

@@ -290,18 +291,30 @@ export class DemoCreator {
290291
})
291292
: stepDescription;
292293

293-
demo.demos.push({
294+
const newDemo: Demo = {
294295
title,
295296
description: description || "",
296297
steps: [...(Array.isArray(step) ? step : [step])],
297-
});
298+
};
299+
300+
if (stepIcons) {
301+
newDemo.icons = stepIcons;
302+
}
303+
304+
demo.demos.push(newDemo);
298305
} else {
299306
if (demo.demos.length === 0) {
300-
demo.demos.push({
307+
const newDemo: Demo = {
301308
title: "New demo",
302309
description: "",
303310
steps: [...(Array.isArray(step) ? step : [step])],
304-
});
311+
};
312+
313+
if (stepIcons) {
314+
newDemo.icons = stepIcons;
315+
}
316+
317+
demo.demos.push(newDemo);
305318
} else {
306319
const demoToEdit = await window.showQuickPick(
307320
demo.demos.map((demo) => demo.title),

src/services/DemoRunner.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,7 @@ export class DemoRunner {
11961196
* @returns A Promise that resolves to an object containing the filePath and demo, or undefined if no demo file is found.
11971197
*/
11981198
private static async getDemoFile(
1199-
item?: ActionTreeItem,
1199+
item?: ActionTreeItem | Uri,
12001200
triggerFirstDemo: boolean = false
12011201
): Promise<
12021202
| {
@@ -1208,25 +1208,28 @@ export class DemoRunner {
12081208
const demoFiles = await FileProvider.getFiles();
12091209
const executingFile = await DemoRunner.getExecutedDemoFile();
12101210

1211-
if (item && item.demoFilePath) {
1211+
const itemPath = item instanceof Uri ? item.fsPath : item?.demoFilePath;
1212+
1213+
if (item && itemPath) {
12121214
if (!demoFiles) {
12131215
Notifications.warning("No demo files found");
12141216
return;
12151217
}
12161218

1217-
const demoFile = await FileProvider.getFile(Uri.file(item.demoFilePath));
1219+
const demoFile = await FileProvider.getFile(Uri.file(itemPath));
12181220
if (!demoFile) {
1219-
Notifications.warning(`No demo file found with the name ${item.description}`);
1221+
const demoFileName = itemPath.split("/").pop();
1222+
Notifications.warning(`No demo file found with the name ${demoFileName}`);
12201223
return;
12211224
}
12221225

1223-
if (executingFile.filePath !== item.demoFilePath) {
1224-
executingFile.filePath = item.demoFilePath;
1226+
if (executingFile.filePath !== itemPath) {
1227+
executingFile.filePath = itemPath;
12251228
executingFile.demo = [];
12261229
await DemoRunner.setExecutedDemoFile(executingFile);
12271230
}
12281231
return {
1229-
filePath: item.demoFilePath,
1232+
filePath: itemPath,
12301233
demo: demoFile,
12311234
};
12321235
} else if (!executingFile.filePath && !item && demoFiles) {

src/services/ImportService.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { commands, FileType, ProgressLocation, Uri, window, workspace } from "vscode";
2+
import { Action, Demos, Subscription } from "../models";
3+
import { Extension } from "./Extension";
4+
import { COMMAND, General } from "../constants";
5+
import { Notifications } from "./Notifications";
6+
import { createImageSlide, parseWinPath } from "../utils";
7+
8+
export class ImportService {
9+
public static register() {
10+
const subscriptions: Subscription[] = Extension.getInstance().subscriptions;
11+
12+
subscriptions.push(commands.registerCommand(COMMAND.importPowerPointImages, ImportService.importPowerPointImages));
13+
}
14+
15+
private static async importPowerPointImages() {
16+
// Ask the user to select the PowerPoint folder
17+
const selectedFolder = await window.showOpenDialog({
18+
canSelectFolders: true,
19+
canSelectMany: false,
20+
openLabel: "Select folder with PowerPoint exported slide images",
21+
});
22+
23+
if (!selectedFolder || selectedFolder.length === 0) {
24+
Notifications.error("No folder selected.");
25+
return;
26+
}
27+
28+
await window.withProgress(
29+
{
30+
location: ProgressLocation.Notification,
31+
title: "Importing images...",
32+
cancellable: false,
33+
},
34+
async (_) => {
35+
const folderUri = selectedFolder[0];
36+
const wsFolder = Extension.getInstance().workspaceFolder;
37+
38+
if (!wsFolder) {
39+
window.showErrorMessage("No workspace folder found.");
40+
return;
41+
}
42+
43+
const files = await workspace.fs.readDirectory(folderUri);
44+
const imageFiles = files.filter(
45+
([name, type]) => type === FileType.File && /\.(jpg|jpeg|png|gif)$/i.test(name)
46+
);
47+
if (imageFiles.length === 0) {
48+
Notifications.error("No image files found in the selected folder.");
49+
return;
50+
}
51+
52+
const createNewDemoFile = await window.showInformationMessage(
53+
"Do you want to add the slides in a new demo file?",
54+
{ modal: true, detail: "This will create a new demo file with the slides." },
55+
"Yes"
56+
);
57+
58+
let imageUris = imageFiles.map(([name]) => folderUri.with({ path: `${folderUri.path}/${name}` }));
59+
imageUris = imageUris.sort((a, b) => {
60+
const nameA = a.path.split("/").pop() || "";
61+
const nameB = b.path.split("/").pop() || "";
62+
return nameA.localeCompare(nameB, undefined, { numeric: true, sensitivity: "base" });
63+
});
64+
65+
const slideFolder = Uri.joinPath(wsFolder.uri, General.demoFolder, General.slidesFolder);
66+
const slideFolderName = parseWinPath(folderUri.path).split("/").pop() || "";
67+
const newSlideFolder = Uri.joinPath(slideFolder, slideFolderName);
68+
69+
try {
70+
await workspace.fs.createDirectory(newSlideFolder);
71+
} catch (error) {
72+
Notifications.error("Failed to create slide folder.");
73+
return;
74+
}
75+
76+
let demo: Demos | undefined;
77+
if (createNewDemoFile === "Yes") {
78+
demo = {
79+
title: slideFolderName,
80+
description: "Imported from PowerPoint",
81+
demos: [],
82+
};
83+
}
84+
85+
for (const [_, imageUri] of imageUris.entries()) {
86+
const imageName = imageUri.path.split("/").pop() || "";
87+
const imageNameWithoutExtension = imageName.split(".").slice(0, -1).join(".");
88+
const newImageUri = Uri.joinPath(newSlideFolder, "images", imageName);
89+
90+
try {
91+
await workspace.fs.copy(imageUri, newImageUri, {
92+
overwrite: true,
93+
});
94+
} catch (error) {
95+
Notifications.error(`Failed to copy image: ${imageUri.path}`);
96+
return;
97+
}
98+
99+
try {
100+
const slidePath = await createImageSlide(newImageUri, newSlideFolder);
101+
102+
if (slidePath && demo && demo.demos) {
103+
demo.demos.push({
104+
title: imageNameWithoutExtension,
105+
description: "",
106+
icons: {
107+
start: "vm",
108+
end: "pass-filled",
109+
},
110+
steps: [{ action: Action.OpenSlide, path: slidePath }],
111+
});
112+
}
113+
} catch (error) {
114+
Notifications.error(`Failed to create slide for image: ${imageUri.path}`);
115+
return;
116+
}
117+
}
118+
119+
if (demo) {
120+
const demoFileUri = Uri.joinPath(wsFolder.uri, General.demoFolder, `${slideFolderName}.json`);
121+
try {
122+
await workspace.fs.writeFile(demoFileUri, Buffer.from(JSON.stringify(demo, null, 2)));
123+
await window.showTextDocument(demoFileUri, { preview: false });
124+
} catch (error) {
125+
Notifications.error("Failed to create demo file.");
126+
return;
127+
}
128+
}
129+
130+
Notifications.info("Slides created successfully!");
131+
}
132+
);
133+
}
134+
}

src/services/Slides.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,10 @@ layout: ${layout.toLowerCase()}
9494
const addStep = await window.showInformationMessage(
9595
`Slide "${slideTitle}" created. Do you want to add it as a new step to the demo?`,
9696
{ modal: true },
97-
"Yes",
98-
"No"
97+
"Yes"
9998
);
10099

101-
if (addStep === "No") {
100+
if (!addStep) {
102101
return;
103102
}
104103

@@ -110,7 +109,10 @@ layout: ${layout.toLowerCase()}
110109
},
111110
];
112111

113-
await addStepsToDemo(steps, slideTitle, "");
112+
await addStepsToDemo(steps, slideTitle, "", {
113+
start: "vm",
114+
end: "pass-filled",
115+
});
114116
}
115117

116118
private static async viewSlide(item: ActionTreeItem) {

src/services/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ export * from "./DemoRunner";
66
export * from "./DemoStatusBar";
77
export * from "./Extension";
88
export * from "./FileProvider";
9+
export * from "./ImportService";
910
export * from "./Logger";
1011
export * from "./NotesService";
1112
export * from "./Notifications";
13+
export * from "./PdfExportService";
1214
export * from "./ScriptExecutor";
1315
export * from "./Slides";
1416
export * from "./StateManager";

src/utils/addStepsToDemo.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { Step } from "../models";
1+
import { Icons, Step } from "../models";
22
import { DemoPanel } from "../panels/DemoPanel";
33
import { DemoCreator, FileProvider } from "../services";
44

5-
export const addStepsToDemo = async (steps: Step | Step[], title?: string, description?: string) => {
5+
export const addStepsToDemo = async (steps: Step | Step[], title?: string, description?: string, icons?: Icons) => {
66
const demoFile = await FileProvider.demoQuickPick();
77
if (!demoFile?.demo) {
88
return;
99
}
1010
const { filePath, demo } = demoFile;
1111

12-
const updatedDemos = await DemoCreator.askWhereToAddStep(demo, steps, title, description);
12+
const updatedDemos = await DemoCreator.askWhereToAddStep(demo, steps, title, description, icons);
1313
if (!updatedDemos) {
1414
return;
1515
}

src/utils/createImageSlide.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Uri, workspace } from "vscode";
2+
import { getRelPath } from "./getRelPath";
3+
4+
export const createImageSlide = async (imageUri: Uri, slideFolder: Uri): Promise<string | undefined> => {
5+
const imageName = imageUri.path.split("/").pop() || "";
6+
const imageNameWithoutExtension = imageName.split(".").slice(0, -1).join(".");
7+
const slideFileName = `${imageNameWithoutExtension}.md`;
8+
const slideFileUri = Uri.joinPath(slideFolder, slideFileName);
9+
const imageRelativePath = getRelPath(imageUri.fsPath);
10+
11+
const contents = `---
12+
theme: default
13+
layout: image
14+
image: ${imageRelativePath}
15+
---`;
16+
17+
try {
18+
await workspace.fs.writeFile(slideFileUri, Buffer.from(contents));
19+
const slideRelativePath = getRelPath(slideFileUri.fsPath);
20+
return slideRelativePath;
21+
} catch (error) {
22+
console.error("Failed to create slide file:", error);
23+
return undefined;
24+
}
25+
};

0 commit comments

Comments
 (0)