Skip to content

Commit

Permalink
feat: custom SVG icons for connections, closes #663
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabio286 committed Jul 11, 2024
1 parent f7419d8 commit 171b6f9
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 26 deletions.
4 changes: 2 additions & 2 deletions src/main/ipc-handlers/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ export default () => {
shortCutRegister.unregister();
});

ipcMain.handle('read-file', (event, filePath) => {
ipcMain.handle('read-file', (event, { filePath, encoding }) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
const content = fs.readFileSync(filePath, 'utf-8');
const content = fs.readFileSync(filePath, encoding);
return content;
}
catch (error) {
Expand Down
27 changes: 25 additions & 2 deletions src/renderer/components/BaseIcon.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
<template>
<SvgIcon
v-if="type === 'mdi'"
:type="type"
:path="iconPath"
:size="size"
:rotate="rotate"
:class="iconFlip"
/>
<svg
v-else
:width="size"
:height="size"
:viewBox="`0 0 ${size} ${size}`"
v-html="iconPath"
/>
</template>

<script setup lang="ts">
import SvgIcon from '@jamescoyle/vue-icon';
import * as Icons from '@mdi/js';
import { computed, PropType } from 'vue';
import { useConnectionsStore } from '@/stores/connections';
const { getIconByUid } = useConnectionsStore();
const props = defineProps({
iconName: {
type: String,
Expand All @@ -23,7 +35,7 @@ const props = defineProps({
default: 48
},
type: {
type: String,
type: String as PropType<'mdi' | 'custom'>,
default: () => 'mdi'
},
flip: {
Expand All @@ -37,7 +49,18 @@ const props = defineProps({
});
const iconPath = computed(() => {
return (Icons as {[k:string]: string})[props.iconName];
if (props.type === 'mdi')
return (Icons as {[k:string]: string})[props.iconName];
else if (props.type === 'custom') {
const base64 = getIconByUid(props.iconName)?.base64;
const svgString = Buffer
.from(base64, 'base64')
.toString('utf-8')
.replaceAll(/width="[^"]*"|height="[^"]*"/g, '');
return svgString;
}
return null;
});
const iconFlip = computed(() => {
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/ModalAllConnections.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
>
<BaseIcon
:icon-name="camelize(connection.icon)"
:type="connection.hasCustomIcon ? 'custom' : 'mdi'"
:size="42"
/>
</div>
Expand Down Expand Up @@ -278,12 +279,15 @@ const remappedConnections = computed(() => {
.map(c => {
const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0;
const connIcon = connectionsOrder.value.find((co) => co.uid === c.uid).icon;
const connHasCustomIcon = connectionsOrder.value.find((co) => co.uid === c.uid).hasCustomIcon;
const folder = folders.value.find(f => f.connections.includes(c.uid));
return {
...c,
icon: connIcon,
color: folder?.color,
folderName: folder?.name,
hasCustomIcon: connHasCustomIcon,
time: connTime
};
})
Expand Down
92 changes: 81 additions & 11 deletions src/renderer/components/ModalConnectionAppearance.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,46 @@
class="icon-box"
:title="icon.name"
:class="[{'selected': localConnection.icon === icon.code}]"
@click="localConnection.icon = icon.code"
@click="setIcon(icon.code)"
/>
<div
v-else
class="icon-box"
:title="icon.name"
:class="[`dbi dbi-${connection.client}`, {'selected': localConnection.icon === icon.code}]"
@click="localConnection.icon = icon.code"
:class="[`dbi dbi-${connection.client}`, {'selected': localConnection.icon === null}]"
@click="setIcon(null)"
/>
</div>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ t('application.customIcon') }}</label>
</div>
<div class="col-9 icons-wrapper">
<div
v-for="icon in customIcons"
:key="icon.uid"
>
<BaseIcon
v-if="icon.uid"
:icon-name="icon.uid"
type="custom"
:size="36"
class="icon-box"
:class="[{'selected': localConnection.icon === icon.uid}]"
@click="setIcon(icon.uid, 'custom')"
@contextmenu.prevent="contextMenu($event, icon.uid)"
/>
</div>
<BaseIcon
:icon-name="'mdiPlus'"
:size="36"
class="icon-box"
@click="openFile"
/>
</div>
</div>
</form>
</div>
</div>
Expand All @@ -74,20 +102,45 @@
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="removeIconHandler">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiDelete"
:size="18"
/> {{ t('general.delete') }}</span>
</div>
</BaseContextMenu>
</Teleport>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { onBeforeUnmount, PropType, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Application from '@/ipc-api/Application';
import { camelize } from '@/libs/camelize';
import { unproxify } from '@/libs/unproxify';
import { SidebarElement, useConnectionsStore } from '@/stores/connections';
const connectionsStore = useConnectionsStore();
const { addIcon, removeIcon, updateConnectionOrder, getConnectionName } = connectionsStore;
const { customIcons } = storeToRefs(connectionsStore);
const isContext = ref(false);
const contextContent: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const { t } = useI18n();
const props = defineProps({
Expand All @@ -99,8 +152,6 @@ const props = defineProps({
const emit = defineEmits(['close']);
const { updateConnectionOrder, getConnectionName } = connectionsStore;
const icons = [
{ name: 'default', code: null },
Expand Down Expand Up @@ -160,14 +211,33 @@ const editFolderAppearance = () => {
closeModal();
};
const camelize = (text: string) => {
const textArr = text.split('-');
for (let i = 0; i < textArr.length; i++) {
if (i === 0) continue;
textArr[i] = textArr[i].charAt(0).toUpperCase() + textArr[i].slice(1);
const setIcon = (code: string, type?: 'mdi' | 'custom') => {
localConnection.value.icon = code;
localConnection.value.hasCustomIcon = type === 'custom';
};
const removeIconHandler = () => {
if (localConnection.value.icon === contextContent.value) {
setIcon(null);
updateConnectionOrder(localConnection.value);
}
removeIcon(contextContent.value);
isContext.value = false;
};
const openFile = async () => {
const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: '"SVG"', extensions: ['svg'] }] });
if (result && !result.canceled) {
const file = result.filePaths[0];
const content = await Application.readFile({ filePath: file, encoding: 'base64url' });
addIcon(content);
}
};
return textArr.join('');
const contextMenu = (event: MouseEvent, iconUid: string) => {
contextEvent.value = event;
contextContent.value = iconUid;
isContext.value = true;
};
const closeModal = () => emit('close');
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/components/ModalSettingsDataExport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ const emit = defineEmits(['close']);
const { trapRef } = useFocusTrap();
const { getConnectionName } = useConnectionsStore();
const { connectionsOrder, connections } = storeToRefs(useConnectionsStore());
const { connectionsOrder, connections, customIcons } = storeToRefs(useConnectionsStore());
const localConnections = unproxify<ConnectionParams[]>(connections.value);
const localConnectionsOrder = unproxify<SidebarElement[]>(connectionsOrder.value);
Expand Down Expand Up @@ -246,7 +246,8 @@ const exportData = () => {
const exportObj = encrypt(JSON.stringify({
connections: filteredConnections,
connectionsOrder: filteredOrders
connectionsOrder: filteredOrders,
customIcons
}), options.value.passkey);
// console.log(exportObj, JSON.parse(decrypt(exportObj, options.value.passkey)));
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/ModalSettingsDataImport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseUploadInput from '@/components/BaseUploadInput.vue';
import { unproxify } from '@/libs/unproxify';
import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { CustomIcon, SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications';
const { t } = useI18n();
Expand Down Expand Up @@ -156,6 +156,7 @@ const importData = () => {
const importObj: {
connections: ConnectionParams[];
connectionsOrder: SidebarElement[];
customIcons: CustomIcon[];
} = JSON.parse(decrypt(hash, options.value.passkey));
if (options.value.ignoreDuplicates) {
Expand Down
1 change: 1 addition & 0 deletions src/renderer/components/SettingBarConnections.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
>
<BaseIcon
:icon-name="camelize(element.icon)"
:type="element.hasCustomIcon ? 'custom' : 'mdi'"
:size="36"
/>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/renderer/components/SettingBarConnectionsFolder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
>
<BaseIcon
:icon-name="camelize(getConnectionOrderByUid(element).icon)"
:type="getConnectionOrderByUid(element).hasCustomIcon ? 'custom' : 'mdi'"
:size="36"
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/WorkspaceTabQuery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ const openFile = async () => {
const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'SQL', extensions: ['sql', 'txt'] }] });
if (result && !result.canceled) {
const file = result.filePaths[0];
const content = await Application.readFile(file);
const content = await Application.readFile({ filePath: file, encoding: 'utf-8' });
const fileName = file.split('/').pop().split('\\').pop();
if (props.tab.filePath && props.tab.filePath !== file) {
newTab({
Expand Down Expand Up @@ -755,7 +755,7 @@ const saveFile = async () => {
};
const loadFileContent = async (file: string) => {
const content = await Application.readFile(file);
const content = await Application.readFile({ filePath: file, encoding: 'utf-8' });
query.value = content;
lastSavedQuery.value = content;
};
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ export const enUS = {
color: 'Color',
label: 'Label',
icon: 'Icon',
customIcon: 'Custom icon',
fileName: 'File name',
choseFile: 'Choose file',
data: 'Data',
Expand Down Expand Up @@ -409,7 +410,8 @@ export const enUS = {
openFile: 'Open file',
openNotes: 'Open notes',
debugConsole: 'Debug console', // <- console tab name
executedQueries: 'Executed queries' // <- console tab name
executedQueries: 'Executed queries', // <- console tab name
sizeLimitError: 'Maximum size of {size} exceeded'
},
faker: { // Faker.js methods, used in random generated content
address: 'Address',
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/ipc-api/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export default class {
return ipcRenderer.invoke('unregister-shortcuts');
}

static readFile (path: string): Promise<string> {
return ipcRenderer.invoke('read-file', path);
static readFile (params: {filePath: string; encoding: string}): Promise<string> {
return ipcRenderer.invoke('read-file', params);
}

static writeFile (path: string, content: unknown) {
Expand Down
Loading

0 comments on commit 171b6f9

Please sign in to comment.