Skip to content

Commit

Permalink
Added windows app icon service
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverschwendener committed Jan 4, 2019
1 parent b13a617 commit 115f27c
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 121 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"typescript": "^3.2.2",
"vue": "^2.5.21",
"webpack": "^4.28.3",
"webpack-cli": "^3.1.2"
"webpack-cli": "^3.1.2",
"windows-system-icon": "^0.0.5"
}
}
34 changes: 33 additions & 1 deletion src/main/helpers/file-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Stats, readdir, lstat } from "fs";
import { Stats, readdir, lstat, unlink, exists } from "fs";
import { join, extname } from "path";

interface FileStat {
Expand Down Expand Up @@ -51,6 +51,38 @@ export class FileHelpers {
});
}

public static getFilesFromFolder(folderPath: string): Promise<string[]> {
return new Promise((resolve, reject) => {
readdir(folderPath, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
}

public static deleteFile(filePath: string): Promise<void> {
return new Promise((resolve, reject) => {
unlink(filePath, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

public static fileExists(filePath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
exists(filePath, (fileExists) => {
resolve(fileExists);
});
});
}

private static handleFileStat(fileStat: FileStat): Promise<string[]> {
return new Promise((resolve, reject) => {
const isFile = fileStat.stats.isFile() || extname(fileStat.filePath) === ".app";
Expand Down
20 changes: 6 additions & 14 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { ApplicationSearchPlugin } from "./search-plugins/application-search-plu
import { FileApplicationRepository } from "./search-plugins/application-search-plugin/file-application-repository";
import { defaultUserConfigOptions } from "../common/config/default-user-config-options";
import { SearchResultItem } from "../common/search-result-item";
import { MacOsApplicationIconService } from "./search-plugins/application-search-plugin/mac-os-application-icon-service";
import { ApplicationIconService } from "./search-plugins/application-search-plugin/application-icon-service";
import { getWindowsAppIcons } from "./search-plugins/application-search-plugin/application-icon-helpers";

let window: BrowserWindow;
const config = defaultUserConfigOptions;
const searchEngine = new SearchEngine([
new ApplicationSearchPlugin(new FileApplicationRepository(new MacOsApplicationIconService(), config.applicationSearchOptions)),
new ApplicationSearchPlugin(new FileApplicationRepository(new ApplicationIconService(getWindowsAppIcons), config.applicationSearchOptions)),
], config.generalOptions);

const rescanInterval = setInterval(() => {
Expand All @@ -31,26 +32,17 @@ const getMaxWindowHeight = (): number => {
};

const updateWindowSize = (searchResultCount: number) => {
window.setResizable(true);
const windowHeight = searchResultCount > config.generalOptions.maxSearchResults
? getMaxWindowHeight()
: searchResultCount * config.generalOptions.searchResultHeight + config.generalOptions.userInputHeight;

window.setSize(config.generalOptions.windowWidth, windowHeight);
window.setResizable(false);
};

const quitApp = () => {
clearInterval(rescanInterval);
searchEngine.clearCache()
.then(() => {
// tslint:disable-next-line:no-console
console.log("Successfully cleared caches");
app.quit();
})
.catch((err) => {
// tslint:disable-next-line:no-console
console.log(`Error while clearing caches: ${err}`);
app.quit();
});
};

app.on("ready", () => {
Expand All @@ -59,7 +51,7 @@ app.on("ready", () => {
frame: false,
height: getMaxWindowHeight(),
resizable: false,
transparent: false,
transparent: true,
width: config.generalOptions.windowWidth,
});
window.loadFile(join(__dirname, "..", "main.html"));
Expand Down
2 changes: 1 addition & 1 deletion src/main/search-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class SearchEngine {
});
}

public clearCache(): Promise<void> {
public clearCaches(): Promise<void> {
return new Promise((resolve, reject) => {
const promises = this.plugins.map((p) => p.clearCache());
Promise.all(promises)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Application } from "./application";
import { join } from "path";
import { createHash } from "crypto";
import { homedir } from "os";
import { convert } from "app2png";
import { Icon, generateIcons } from "windows-system-icon";
import { ApplicationIcon } from "./application-icon";

export const applicationIconLocation = join(homedir(), ".ueli", "application-icons");

export function getApplicationIconFilePath(application: Application): string {
const fileHashName = createHash("md5").update(`${application.filePath}`).digest("hex");
return `${join(applicationIconLocation, fileHashName)}.png`;
}

export function getMacAppIcons(applications: Application[]): Promise<ApplicationIcon[]> {
return new Promise((resolve, reject) => {
const promises = applications.map((application) => {
return convert(application.filePath, getApplicationIconFilePath(application));
});

Promise.all(promises)
.then(() => {
resolve(applications.map((application): ApplicationIcon => {
return {
filePathToPng: getApplicationIconFilePath(application),
name: application.name,
};
}));
})
.catch((err) => {
reject(err);
});
});
}

export function getWindowsAppIcons(applications: Application[]): Promise<ApplicationIcon[]> {
return new Promise((resolve, reject) => {
const icons = applications.map((application): Icon => {
return {
inputFilePath: application.filePath,
outputFilePath: getApplicationIconFilePath(application),
outputFormat: "Png",
};
});

generateIcons(icons)
.then(() => {
resolve(applications.map((application): ApplicationIcon => {
return {
filePathToPng: getApplicationIconFilePath(application),
name: application.name,
};
}));
})
.catch((err) => {
reject(err);
});
});
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
import { Application } from "./application";
import { join } from "path";
import { applicationIconLocation } from "./application-icon-helpers";
import { FileHelpers } from "../../helpers/file-helpers";
import { ApplicationIcon } from "./application-icon";

export interface ApplicationIconService {
getIcon(applicaton: Application): Promise<string>;
clearCache(): Promise<void>;
export class ApplicationIconService {
private readonly getAppIcons: (applications: Application[]) => Promise<ApplicationIcon[]>;

constructor(getAppIcons: (applications: Application[]) => Promise<ApplicationIcon[]>) {
this.getAppIcons = getAppIcons;
}

public getIcons(applications: Application[]): Promise<ApplicationIcon[]> {
return new Promise((resolve, reject) => {
this.getAppIcons(applications)
.then((appIcons) => {
resolve(appIcons);
})
.catch((err) => {
reject(err);
});
});
}

public clearCache(): Promise<void> {
return new Promise((resolve, reject) => {
FileHelpers.getFilesFromFolder(applicationIconLocation)
.then((files) => {
const deletionPromises = files.map((file) => FileHelpers.deleteFile(join(applicationIconLocation, file)));
Promise.all(deletionPromises)
.then(() => {
resolve();
})
.catch((deletionError) => {
reject(deletionError);
});
})
.catch((err) => {
reject(err);
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ApplicationIcon {
name: string;
filePathToPng: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,24 @@ export class FileApplicationRepository implements ApplicationRepository {
files = files.concat(fileList);
});

const applicationPromises = files
const applications = files
.filter((file) => this.filterByApplicationFileExtensions(file))
.map((file) => this.createApplicationFromFilePath(file));
.map((applicationFile): Application => this.createApplicationFromFilePath(applicationFile));

Promise.all(applicationPromises)
.then((applicationList) => {
this.applications = applicationList;
this.applicationIconService.getIcons(applications)
.then((appIcons) => {
applications.forEach((application) => {
const appIcon = appIcons.find((a) => a.name === application.name);
if (appIcon !== undefined) {
application.icon = appIcon.filePathToPng;
}
});

this.applications = applications;
resolve();
})
.catch((applicationError) => {
reject(applicationError);
.catch((err) => {
reject(err);
});
})
.catch((applicationError) => {
Expand All @@ -66,23 +73,12 @@ export class FileApplicationRepository implements ApplicationRepository {
});
}

private createApplicationFromFilePath(filePath: string): Promise<Application> {
return new Promise((resolve, reject) => {
const application = {
filePath,
icon: "",
name: basename(filePath).replace(extname(filePath), ""),
};

this.applicationIconService.getIcon(application)
.then((pngFilePath) => {
application.icon = pngFilePath;
resolve(application);
})
.catch(() => {
resolve(application);
});
});
private createApplicationFromFilePath(filePath: string): Application {
return {
filePath,
icon: "",
name: basename(filePath).replace(extname(filePath), ""),
};
}

private filterByApplicationFileExtensions(file: string): boolean {
Expand Down

This file was deleted.

5 changes: 1 addition & 4 deletions src/renderer/search-results-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ export const searchResultsComponent = Vue.extend({
this.searchResults[nextActiveIndex].active = true;
this.scrollIntoView(this.searchResults[nextActiveIndex].id);
},
renderBackgroundImageStyle(searchResult: SearchResultItemViewModel) {
return `background-image: url("${searchResult.icon}")`;
},
scrollIntoView(index: string) {
const userInput = document.getElementById("user-input");
if (userInput !== undefined && userInput) {
Expand Down Expand Up @@ -106,7 +103,7 @@ export const searchResultsComponent = Vue.extend({
template: `
<div class="search-results" :id="containerId">
<div :id="searchResult.id" class="search-results__item" :class="{ 'active' : searchResult.active }" v-for="searchResult in searchResults">
<div class="search-results__item-icon" :style="renderBackgroundImageStyle(searchResult)"></div>
<img class="search-results__item-icon" :src="searchResult.icon">
{{ searchResult.name }}
</div>
</div>
Expand Down
Loading

0 comments on commit 115f27c

Please sign in to comment.