Membuat aplikasi dekstop dengan electron js

Galih Setiawan Nurohim
6 min readDec 2, 2024

--

mkdir test-app
cd test-app
npm init -y

Pasang Electron:

npm install electron --save-dev

Buat file utama: Buat file bernama main.js di root proyek kamu:

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
},
});

// Memuat file index.html
win.loadFile('index.html');
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

Buat file HTML utama: Buat file index.html di root proyek kamu:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Markdown App</title>
</head>
<body>
<div id="app">
<textarea id="editor" placeholder="Write your notes here..." style="width: 100%; height: 90vh;"></textarea>
<div id="preview" style="border-top: 1px solid #ccc; height: 10vh; overflow-y: auto;"></div>
</div>

<script src="renderer.js"></script>
</body>
</html>

Tambahkan renderer script: Buat file renderer.js untuk menangani logika frontend:

const textarea = document.getElementById('editor');
const preview = document.getElementById('preview');

textarea.addEventListener('input', () => {
const text = textarea.value;
preview.innerHTML = convertMarkdownToHTML(text);
});

function convertMarkdownToHTML(markdown) {
// Gunakan library Markdown seperti marked.js untuk merender
// npm install marked
const marked = require('marked');
return marked(markdown);
}

Tambahkan library markdown: Kamu bisa menggunakan library seperti marked untuk merender Markdown ke HTML.

Instal library marked:

npm install marked

Periksa File package.json

  • astikan bahwa package.json memiliki entri "main" yang menunjuk ke file utama aplikasi, biasanya main.js atau nama lain yang sesuai.
  • Contoh konfigurasi package.json yang benar:
{
"name": "test-app",
"version": "1.0.0",
"description": "A simple Electron app",
"main": "main.js", // pastikan nama file utama benar
"scripts": {
"start": "electron ."
},
"dependencies": {
"electron": "^latest"
}
}

Jalankan

npm start

Rencananya pengen bikin app semacam obsidian, membuat mindmap berdasarkan list yang dibuat. berarti butuh library tambahan yakni d3.js, bisa simpan .pngjuga untuk

Berarti pasang electron dan d3.js

npm install electron d3 --save-dev

Edit file index.html: Buat file HTML untuk antarmuka dasar aplikasi.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mindmap Generator</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
}
#editor {
width: 90%;
height: 150px;
margin-top: 20px;
}
#mindmap {
width: 90%;
height: 600px;
margin-top: 20px;
border: 1px solid #ccc;
}
button {
margin-top: 10px;
}
</style>
</head>
<body>
<h1>Mindmap Generator</h1>
<textarea id="editor" placeholder="- Root Node
-- Child Node 1
--- Sub-Child Node 1.1
--- Sub-Child Node 1.2
---- Sub-Sub-Child Node 1.2.1
-- Child Node 2
--- Sub-Child Node 2.1"></textarea>
<button onclick="generateMindmap()">Generate Mindmap</button>
<button onclick="saveAsImage()">Save as Image</button>
<div id="mindmap"></div>

<script src="renderer.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
</body>
</html>

Bikin file renderer.js: Tambahkan logika untuk memproses input teks menjadi mindmap menggunakan D3.js.

function generateMindmap() {
// Ambil input dari textarea dan pecah menjadi array baris
const input = document.getElementById('editor').value; // ambil nilai elemen textarea dengan id 'editor'
const lines = input.split('\n').filter(line => line.trim() !== ''); // Pecah input menjadi array baris dan hilangkan baris kosong

// Struktur root node sebagai awal mindmap
const root = { name: 'Root', children: [] }; // Node utama dengan array children kosong
let stack = [{ node: root, level: -1 }]; // Stack digunakan untuk melacak node parent berdasarkan level

// Loop untuk mengubah setiap baris input menjadi node dalam tree
lines.forEach(line => {
const trimmedLine = line.trim(); // Hapus spasi di awal dan akhir
const level = (line.match(/^-+/) || [''])[0].length - 1; // Tentukan level berdasarkan jumlah tanda "-"
const node = { name: trimmedLine.replace(/^-+/, '').trim(), children: [] }; // Buat node baru

// Cari parent node yang sesuai dengan level saat ini
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
stack.pop(); // Kembali ke node parent di stack yang memiliki level lebih rendah
}

// Tambahkan node ke parent saat ini yang ditemukan di stack
if (stack.length > 0) {
stack[stack.length - 1].node.children.push(node);
}

// Tambahkan node baru ke stack dengan level saat ini
stack.push({ node, level });
});

const nodes = [];
const links = [];

// Fungsi untuk melakukan traversal tree dan mengisi nodes dan links untuk D3.js
function traverse(node, parent = null) {
nodes.push(node); // Tambahkan node ke array nodes
if (parent) {
links.push({ source: parent, target: node }); // Tambahkan link antara parent dan child
}
if (node.children) {
node.children.forEach(child => traverse(child, node)); // Rekursif untuk setiap anak
}
}

traverse(root); // Mulai traversal dari root

// Render mindmap sebagai force-directed graph menggunakan D3.js
renderForceDirectedGraph(nodes, links);
}

function renderForceDirectedGraph(nodes, links) {
// Ambil ukuran elemen mindmap untuk menentukan ukuran canvas SVG
const width = document.getElementById('mindmap').clientWidth;
const height = document.getElementById('mindmap').clientHeight;

// Bersihkan konten sebelumnya di elemen mindmap
document.getElementById('mindmap').innerHTML = '';

// Buat elemen SVG untuk visualisasi graf
const svg = d3
.select('#mindmap')
.append('svg')
.attr('width', width)
.attr('height', height);

// Buat simulasi graf force-directed dengan D3.js
const simulation = d3
.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.name).distance(100)) // Buat gaya link antara nodes
.force('charge', d3.forceManyBody().strength(-300)) // Atur gaya tolak-menolak antar nodes
.force('center', d3.forceCenter(width / 2, height / 2)); // Pusatkan graf

// Tambahkan elemen link (garis penghubung) ke graf
const link = svg
.append('g')
.attr('stroke', '#ccc')
.selectAll('line')
.data(links)
.enter()
.append('line');

// Tambahkan elemen node (lingkaran) ke graf
const node = svg
.append('g')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', 5) // Ukuran node
.attr('fill', '#69b3a2') // Warna node
.call(
d3.drag() // Tambahkan event drag ke nodes
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
);

// Tambahkan label teks ke node
const label = svg
.append('g')
.selectAll('text')
.data(nodes)
.enter()
.append('text')
.text(d => d.name) // Tampilkan nama node
.attr('x', 8)
.attr('y', 3)
.attr('font-size', '10px');

// Fungsi untuk memperbarui posisi elemen saat simulasi berjalan
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);

node
.attr('cx', d => d.x)
.attr('cy', d => d.y);

label
.attr('x', d => d.x + 8)
.attr('y', d => d.y + 3);
});

// Fungsi drag untuk node
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart(); // Aktifkan simulasi
d.fx = d.x; // Tetapkan posisi x tetap selama drag
d.fy = d.y; // Tetapkan posisi y tetap selama drag
}

function dragged(event, d) {
d.fx = event.x; // Perbarui posisi x selama drag
d.fy = event.y; // Perbarui posisi y selama drag
}

function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0); // Matikan simulasi
d.fx = null; // Hapus posisi tetap setelah drag selesai
d.fy = null;
}
}

function saveAsImage() {
// Ambil elemen SVG
const svg = document.querySelector('#mindmap svg');

// Periksa apakah mindmap sudah di-generate
if (!svg) {
alert('Mindmap belum dibuat.');
return;
}

// Konversi elemen SVG ke string
const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

// Set ukuran canvas sesuai ukuran SVG
const svgSize = svg.getBoundingClientRect();
canvas.width = svgSize.width;
canvas.height = svgSize.height;

// Tambahkan latar belakang putih
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Buat gambar dari data SVG
const img = new Image();
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);

img.onload = function () {
// Gambar SVG ke canvas
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);

// Buat dan klik tautan unduhan untuk menyimpan gambar
const link = document.createElement('a');
link.download = 'mindmap.png';
link.href = canvas.toDataURL('image/png');
link.click();
};

img.src = url; // Set sumber gambar dari data SVG
}

Lalu penting untuk menyesuaikan package.json

{
"name": "obsidian",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"d3": "^7.9.0",
"electron": "^33.2.0"
},
"dependencies": {
"marked": "^15.0.0"
}
}

Oke karena referensinya Obsidian saya akan bikin bagian kiri input list text bagian kanan visualisasi mindmapnya, live berubah maka ada perubahan yang perlu dilakukan di renderer.json tambahkan Event Listener untuk merender mindmap secara langsung saat input teks diubah oleh user.

document.getElementById('editor').addEventListener('input', generateMindmap);

Lalu image nya bisa disimpan perlu Event Listener juga di bagian elemen mindmap

Tambahkan di renderer.json

// Tambahkan event listener untuk klik kanan pada elemen mindmap
document.getElementById('mindmap').addEventListener('contextmenu', function (event) {
event.preventDefault(); // Mencegah menu konteks default
saveAsImage(); // Panggil fungsi untuk menyimpan gambar
});

--

--

Galih Setiawan Nurohim
Galih Setiawan Nurohim

No responses yet