【Windows/Mac対応】Electronでつくる便利なファイル共有アプリ【自作アプリ】〜実践編〜

目次

概要

Electronというフレームワークを使って、JavaScript、HTML、CSSの知識でアプリを作成したので紹介します。

どんなアプリを作成したかというと、同じネットワーク内のデバイスでファイルのやり取りができるアプリです!

簡単に言えばair dropみたいなイメージでしょうか。

Electronについては前回の記事で紹介しているのでチェックしてみてね!

成果物

起動画面

アクセスできるのは同じネットワークに接続してる人だけです。

わかりやすくいうなら、同じWi-Fiに繋いでる人がファイルのやり取りができます。

アプリを起動しておけば別端末からアクセスすることができます。

動画のプレビュー機能もあります。

完成品をワンコインで公開

めんどくさい方向けに完成品を販売します。

準備中

いっさいの責任も負いません。また、サポートもありません。ご理解の上購入してください。

また、似たような無料ソフトもあります。よく考えて買ってください。

管理人作のように動画のプレビュー機能はないみたいですが。

コード

ディレクトリ構造

.
├── assets/
│   └── app-icon.icns
├── node_modules/
├── public/
│   ├── app-icon.png
│   ├── index.html
│   ├── script.js
│   └── style.css
├── uploads/
├── index.js
├── package-lock.json
└── package.json
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ファイル保管庫 - ファイル共有</title>
    <link rel="icon" type="image/x-icon" href="app-icon.png">
    <link rel="stylesheet" href="style.css">
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
    <div class="container">
        <header class="header">
            <h1>ファイル保管庫</h1>
            <div class="header-right">
                <div class="link-group">
                    <div id="qrcode-container"></div>
                    <a id="site-address-link" class="site-address-link" href="#" target="_blank"></a>
                </div>
            </div>
        </header>

        <main class="main-content">
            <section class="upload-section card sidebar">
                <h2>ファイルのアップロード</h2>
                <div class="upload-form">
                    <input type="file" id="file-input" name="file" class="file-input">
                    <button id="upload-button" class="btn primary-btn">アップロード開始</button>
                </div>
                <div id="upload-progress-bar-container" class="progress-container">
                    <div id="upload-progress-bar" class="progress-bar"></div>
                    <span id="upload-status" class="progress-status"></span>
                </div>
            </section>
    
            <section class="file-list-section card content-view">
                <div class="file-list-header">
                    <h2>保管庫の内容</h2>
                    <div class="file-list-controls">
                        <input type="text" id="search-input" class="search-input" placeholder="ファイル名で検索...">
                        <button id="refresh-button" class="btn secondary-btn">更新</button>
                    </div>
                </div>
                <ul id="file-list" class="file-list">
                </ul>
            </section>
        </main>
    </div>

    <div id="modal-backdrop" class="modal-backdrop">
        <div class="modal-content">
            <button class="modal-close-btn" id="modal-close-btn">×</button>
            <div id="modal-player-container" class="modal-player-container"></div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
    <script src="script.js"></script>
</body>
</html>
CSS
/* === グローバル設定とフォント === */
body {
    font-family: 'Noto Sans JP', sans-serif;
    margin: 0;
    padding: 0;
    background-color: #1a1a2e;
    color: #e0e0e0;
    line-height: 1.6;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    overflow-y: auto;
}

.container {
    width: 90%;
    max-width: 800px;
    padding: 30px;
    box-sizing: border-box;
}

/* === ヘッダー === */
.header {
    text-align: center;
    margin-bottom: 40px;
    color: #e0e0e0;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.header h1 {
    font-size: 2.8em;
    color: #8be9fd;
    margin: 0;
    text-shadow: 0 0 10px rgba(139, 233, 253, 0.4);
    font-weight: 700;
}

/* アドレスとQRコードをまとめるコンテナ */
.header-right {
    display: flex;
    /* 項目が一つになったので、flex-direction: column を削除 */
    align-items: flex-end; /* 右寄せに変更 */
    gap: 15px;
}

/* サイトアドレスのスタイル */
.site-address-link {
    font-size: 1em;
    color: #b3b3c9;
    text-decoration: none;
    transition: color 0.3s ease;
}

.site-address-link:hover {
    color: #8be9fd;
}

/* QRコードのコンテナのスタイル */
#qrcode-container {
    width: 60px;
    height: 60px;
    padding: 2px;
    background-color: #fff;
    border-radius: 4px;
    border: 1px solid rgba(139, 233, 253, 0.2);
    transition: transform 0.2s ease;
}

#qrcode-container:hover {
    transform: scale(1.1);
}

/* === カード(セクションの背景)=== */
.card {
    background-color: #2a2a4a;
    border-radius: 12px;
    padding: 30px;
    margin-bottom: 30px;
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
    border: 1px solid rgba(139, 233, 253, 0.1);
}

.card h2 {
    font-size: 1.8em;
    color: #f8f8f2;
    margin-bottom: 25px;
    border-bottom: 2px solid #8be9fd;
    padding-bottom: 10px;
    text-align: center;
}

/* === アップロードセクション === */
.upload-form {
    display: flex;
    flex-direction: column;
    gap: 15px;
    align-items: center;
}

.file-input {
    display: block;
    width: 100%;
    max-width: 350px;
    padding: 12px;
    background-color: #3b3b5b;
    color: #f8f8f2;
    border: 1px solid #6272a4;
    border-radius: 8px;
    font-size: 1em;
    cursor: pointer;
    transition: all 0.3s ease;
}

.file-input::-webkit-file-upload-button {
    background-color: #6272a4;
    color: #f8f8f2;
    padding: 8px 15px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    margin-right: 10px;
    transition: background-color 0.2s ease;
}

.file-input::-webkit-file-upload-button:hover {
    background-color: #4a5a8a;
}

.file-input:hover {
    border-color: #8be9fd;
    box-shadow: 0 0 8px rgba(139, 233, 253, 0.3);
}

/* === ボタン === */
.btn {
    padding: 12px 25px;
    border: none;
    border-radius: 8px;
    font-size: 1.1em;
    cursor: pointer;
    transition: all 0.3s ease;
    font-weight: 600;
    width: 100%;
    max-width: 200px;
    text-align: center;
}

.btn-tiny {
    padding: 8px 12px;
    font-size: 0.9em;
    width: auto;
    max-width: none;
}

.primary-btn {
    background-color: #8be9fd;
    color: #1a1a2e;
    box-shadow: 0 0 15px rgba(139, 233, 253, 0.5);
}

.primary-btn:hover {
    background-color: #62d8f8;
    box-shadow: 0 0 25px rgba(139, 233, 253, 0.7);
    transform: translateY(-2px);
}

.secondary-btn {
    background-color: #3b3a56;
    color: #8be9fd;
}

.secondary-btn:hover {
    background-color: #4a496a;
}

.tertiary-btn {
    background-color: #ff5555;
    color: #1a1a2e;
}

.tertiary-btn:hover {
    background-color: #ff3333;
}

/* === プログレスバー === */
.progress-container {
    width: 100%;
    max-width: 350px;
    margin: 20px auto 0;
    background-color: #3b3b5b;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.3);
    height: 35px;
    position: relative;
    display: none;
}

.progress-bar {
    height: 100%;
    width: 0%;
    background-color: #50fa7b;
    transition: width 0.4s ease-out;
    border-radius: 8px;
}

.progress-status {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    text-align: center;
    color: #f8f8f2;
    font-size: 0.9em;
    font-weight: 600;
    z-index: 1;
}

/* === ファイルリスト === */
/* ファイルリストのヘッダーの新しいスタイル */
.file-list-header {
    flex-direction: column;
    margin-bottom: 20px;
    gap: 15px;
}

/* 「保管庫の内容」タイトルが折り返さないように */
.file-list-section h2 {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    width: 100%;
    text-align: center;
}


.file-list-controls {
    display: flex;
    justify-content: center;
    width: 100%;
    gap: 10px;
    flex-wrap: nowrap;
}

/* === 検索入力フィールド === */
.search-input {
    flex-grow: 1;
    min-width: 120px;
    max-width: 250px;
    padding: 10px;
    border: 1px solid #6272a4;
    border-radius: 8px;
    background-color: #3b3a56;
    color: #f8f8f2;
    font-size: 1em;
    transition: border-color 0.3s ease;
}

.search-input::placeholder {
    color: #b3b3c9;
}

.search-input:focus {
    outline: none;
    border-color: #8be9fd;
}

.file-list {
    list-style: none;
    padding: 0;
    margin-top: 20px;
}

.file-item {
    background-color: #2a2a4a;
    border: 1px solid rgba(139, 233, 253, 0.1);
    border-radius: 12px;
    margin-bottom: 25px;
    padding: 20px;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    position: relative;
    justify-content: space-between;
}

.file-item:hover {
    transform: translateY(-3px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
    border-color: #8be9fd;
}

/* ファイル名とアイコンをまとめるコンテナ */
.file-info {
    display: flex;
    align-items: center;
    flex: 1;
    margin-right: 15px;
    word-break: break-all;
    overflow-wrap: break-word;
}

.file-icon {
    font-size: 1.5em;
    margin-right: 15px;
    flex-shrink: 0;
}

.file-name {
    flex: 1;
}

.no-files {
    color: #6272a4;
    text-align: center;
    padding: 20px;
    font-style: italic;
}

/* === 削除ボタン === */
.delete-btn {
    position: absolute;
    top: -10px;
    left: -10px;
    width: 30px;
    height: 30px;
    line-height: 28px;
    padding: 0;
    background-color: #a09b9b;
    color: #1a1a2e;
    border-radius: 50%;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.3s ease, transform 0.3s ease;
    border: none;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
    z-index: 20;
}

.delete-btn:hover {
    background-color: #ff453a;
    transform: scale(1.1);
}

/* === アクションボタン(再生・ダウンロード)=== */
.file-item-buttons {
    display: flex;
    gap: 10px;
    flex-shrink: 0;
}

.play-btn, .download-btn {
    font-size: 0.9em;
    padding: 8px 15px;
    flex-grow: 0;
}

.play-btn {
    background-color: #0a84ff;
    color: #fff;
}

.download-btn {
    background-color: #34c759;
    color: #fff;
}

/* === モーダルウィンドウ === */
.modal-backdrop {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(10px) saturate(180%);
    -webkit-backdrop-filter: blur(10px) saturate(180%);
    display: flex;
    justify-content: center;
    align-items: center;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0.3s ease;
    z-index: 1000;
}

.modal-backdrop.is-visible {
    opacity: 1;
    visibility: visible;
}

.modal-content {
    position: relative;
    background-color: rgba(42, 42, 74, 0.9);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    padding: 20px;
    border-radius: 12px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
    border: 1px solid rgba(139, 233, 253, 0.1);
    max-width: 90%;
    max-height: 90%;
}

.modal-close-btn {
    position: absolute;
    top: -15px;
    right: -15px;
    width: 30px;
    height: 30px;
    background-color: #ff5555;
    color: #1a1a2e;
    border: none;
    border-radius: 50%;
    font-size: 1.2em;
    font-weight: 600;
    cursor: pointer;
    line-height: 28px;
    text-align: center;
    z-index: 1001;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
    transition: background-color 0.2s ease, transform 0.2s ease;
}

.modal-close-btn:hover {
    background-color: #ff3333;
    transform: scale(1.1);
}

.modal-video-player {
    width: 100%;
    height: auto;
    max-height: 70vh;
    border-radius: 8px;
}

/* 旧パスワードUI用スタイルを削除 */
/* .password-group { ... } */
/* .password-input { ... } */

.link-group {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* === アニメーション === */
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}

.fade-in {
    animation: fadeIn 0.4s ease-out forwards;
}

/* === レスポンシブデザイン === */
@media (max-width: 600px) {
    body {
        font-size: 14px;
    }
    .container {
        padding: 15px;
    }
    .header {
        flex-direction: column;
        align-items: center;
    }
    .header-right {
        flex-direction: column;
        align-items: center;
        margin-top: 10px;
        gap: 5px;
    }
    .site-address-link {
        text-align: center;
    }
    .header h1 {
        font-size: 2em;
        margin-bottom: 0;
    }
    .card {
        padding: 20px;
    }
    .card h2 {
        font-size: 1.5em;
    }
    .btn {
        font-size: 1em;
        padding: 10px 20px;
    }
    /* モバイルでのファイルリストヘッダーのレイアウト調整 */
    .file-list-header {
        flex-direction: column;
        align-items: center;
        gap: 10px;
    }
    .file-list-section h2 {
        margin-bottom: 0;
    }
    .file-list-controls {
        flex-direction: row;
        flex-wrap: nowrap;
        width: 100%;
        justify-content: center;
        gap: 8px;
    }
    /* 検索入力欄と更新ボタンの幅を調整 */
    .search-input {
        flex-grow: 1;
        min-width: 100px;
        max-width: 250px;
    }
    .refresh-button {
        flex-shrink: 0;
        min-width: 50px; /* 更新ボタンの最小幅 */
        max-width: 60px; /* 更新ボタンの最大幅を半減 */
        padding: 8px 12px; /* パディングも調整 */
    }
    .file-item {
        flex-direction: column;
        align-items: flex-start;
        padding: 15px;
        padding-top: 40px;
    }
    .file-info {
        width: 100%;
        margin-right: 0;
        margin-bottom: 10px;
    }
    .file-item-buttons {
        width: 100%;
        justify-content: flex-start;
        margin-left: 0;
        margin-top: 10px;
    }
    .action-btn {
        min-width: 60px;
    }
    .delete-btn {
        width: 30px;
        height: 30px;
        line-height: 30px;
        padding: 0;
        border: none;
        border-radius: 50%;
        font-size: 1.2em;
        font-weight: 600;
        text-align: center;
        color: #fff;
        background-color: #a09b9b;
        flex-shrink: 0;
        transition: all 0.2s ease-in-out;
    }
}
JavaScript
const fileInput = document.getElementById('file-input');
const uploadButton = document.getElementById('upload-button');
const fileList = document.getElementById('file-list');
const progressBarContainer = document.getElementById('upload-progress-bar-container');
const progressBar = document.getElementById('upload-progress-bar');
const statusText = document.getElementById('upload-status');
const modalBackdrop = document.getElementById('modal-backdrop');
const modalCloseBtn = document.getElementById('modal-close-btn');
const modalPlayerContainer = document.getElementById('modal-player-container');
const refreshButton = document.getElementById('refresh-button');
const searchInput = document.getElementById('search-input');

// === グローバル変数 ===
let allFiles = [];

// === ユーティリティ関数 ===
const getFileIcon = (fileName) => {
    const extension = fileName.split('.').pop().toLowerCase();
    switch (extension) {
        case 'mp4':
        case 'mov':
        case 'webm':
            return '🎥';
        case 'png':
        case 'jpg':
        case 'jpeg':
        case 'gif':
        case 'svg':
            return '🖼️';
        case 'mp3':
        case 'wav':
        case 'ogg':
            return '🎵';
        case 'pdf':
            return '📄';
        case 'zip':
        case 'tar':
        case 'gz':
            return '📦';
        default:
            return '📝';
    }
};

const playVideoInModal = (file) => {
    modalPlayerContainer.innerHTML = '';
    const videoElement = document.createElement('video');
    videoElement.classList.add('modal-video-player');
    // パスワードクエリを削除
    const url = `/uploads/${file}`;
    videoElement.src = url;
    videoElement.controls = true;
    videoElement.autoplay = true;
    modalPlayerContainer.appendChild(videoElement);
    modalBackdrop.classList.add('is-visible');
};

const closeModal = () => {
    modalBackdrop.classList.remove('is-visible');
    modalPlayerContainer.innerHTML = '';
};

const createListItem = (file) => {
    const listItem = document.createElement('li');
    listItem.classList.add('file-item', 'fade-in');

    const fileInfo = document.createElement('div');
    fileInfo.classList.add('file-info');

    const fileIcon = document.createElement('span');
    fileIcon.classList.add('file-icon');
    fileIcon.textContent = getFileIcon(file);

    const fileName = document.createElement('span');
    fileName.classList.add('file-name');
    fileName.textContent = file;

    fileInfo.appendChild(fileIcon);
    fileInfo.appendChild(fileName);

    const deleteButton = document.createElement('button');
    deleteButton.textContent = 'X';
    deleteButton.classList.add('btn', 'delete-btn');
    deleteButton.addEventListener('click', async (e) => {
        e.stopPropagation();
        if (confirm(`${file}を本当に削除しますか?`)) {
            // パスワードクエリを削除
            let url = `/files/${file}`;
            
            try {
                const response = await fetch(url, { method: 'DELETE' });
                if (response.ok) {
                    console.log('ファイルが正常に削除されました。');
                    fetchFileList();
                } else {
                    alert('ファイルの削除に失敗しました。');
                }
            } catch (error) {
                console.error('削除リクエストエラー:', error);
                alert('ファイルの削除中にエラーが発生しました。');
            }
        }
    });

    const buttonsContainer = document.createElement('div');
    buttonsContainer.classList.add('file-item-buttons');

    const downloadButton = document.createElement('a');
    downloadButton.textContent = 'DL';
    downloadButton.classList.add('btn', 'download-btn');
    // パスワードクエリを削除
    downloadButton.href = `/uploads/${file}`;
    downloadButton.setAttribute('download', file);

    buttonsContainer.appendChild(downloadButton);

    const fileExtension = file.split('.').pop().toLowerCase();
    if (['mp4', 'mov', 'webm'].includes(fileExtension)) {
        const playButton = document.createElement('button');
        playButton.textContent = 'Play!';
        playButton.classList.add('btn', 'play-btn');
        playButton.addEventListener('click', () => playVideoInModal(file));
        buttonsContainer.insertBefore(playButton, downloadButton);
    }
    
    listItem.appendChild(fileInfo);
    listItem.appendChild(buttonsContainer);
    listItem.appendChild(deleteButton);

    return listItem;
};

const renderFileList = (files) => {
    fileList.innerHTML = '';
    if (files.length === 0) {
        fileList.innerHTML = '<li class="no-files">ファイルが見つかりません。</li>';
        return;
    }
    files.forEach(file => {
        const listItem = createListItem(file);
        fileList.appendChild(listItem);
    });
};

// === メインの処理 ===
const fetchFileList = async () => {
    let url = '/files';
    
    try {
        const response = await fetch(url);
        if (response.ok) {
            allFiles = await response.json();
            renderFileList(allFiles);
        } else {
            console.error('ファイルリストの取得に失敗しました。');
            fileList.innerHTML = '<li class="no-files">ファイルリストの取得に失敗しました。</li>';
        }
    } catch (error) {
        console.error('ファイルリスト取得エラー:', error);
    }
};

const fetchAndDisplayIpAddress = async () => {
    const linkElement = document.getElementById('site-address-link');
    const qrCodeContainer = document.getElementById('qrcode-container');
    
    try {

        const response = await fetch('/ip-address');
        const data = await response.json();
        const ipAddress = data.ipAddress;
        
        let url = `http://${ipAddress}:3000/`;
        
        linkElement.textContent = url;
        linkElement.href = url;

        qrCodeContainer.innerHTML = '';
        new QRCode(qrCodeContainer, {
            text: url,
            width: 56,
            height: 56,
            colorDark : "#1a1a2e",
            colorLight : "#ffffff",
            correctLevel : QRCode.CorrectLevel.H
        });

    } catch (error) {
        console.error('IPアドレスの取得に失敗しました。', error);
        linkElement.textContent = 'アドレス取得エラー';
        qrCodeContainer.style.display = 'none';
    }
};

// === イベントリスナー ===
searchInput.addEventListener('input', (e) => {
    const searchTerm = e.target.value.toLowerCase();
    const filteredFiles = allFiles.filter(file => file.toLowerCase().includes(searchTerm));
    renderFileList(filteredFiles);
});

refreshButton.addEventListener('click', () => {
    fetchFileList();
});

uploadButton.addEventListener('click', () => {
    const file = fileInput.files[0];
    if (!file) {
        alert('ファイルを選択してください。');
        return;
    }

    const formData = new FormData();
    formData.append('file', file);

    progressBarContainer.style.display = 'block';
    progressBar.style.width = '0%';
    statusText.textContent = 'アップロードを開始しています...';

    const xhr = new XMLHttpRequest();

    xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
            const percentComplete = Math.round((e.loaded / e.total) * 100);
            progressBar.style.width = percentComplete + '%';
            statusText.textContent = `アップロード中... ${percentComplete}%`;
        }
    });

    xhr.addEventListener('load', () => {
        if (xhr.status === 200) {
            statusText.textContent = 'アップロードが完了しました!';
            fetchFileList();
        } else {
            statusText.textContent = 'アップロードに失敗しました。';
            alert('アップロードに失敗しました。');
        }
        setTimeout(() => {
            progressBarContainer.style.display = 'none';
            statusText.textContent = '';
        }, 3000); 
    });

    xhr.addEventListener('error', () => {
        statusText.textContent = 'エラーが発生しました。';
        alert('サーバーへの接続に失敗しました。');
        progressBarContainer.style.display = 'none';
    });

    let url = '/upload';
    
    xhr.open('POST', url, true);
    xhr.send(formData);
});

modalCloseBtn.addEventListener('click', closeModal);
modalBackdrop.addEventListener('click', (e) => {
    if (e.target === modalBackdrop) {
        closeModal();
    }
});

// ページが完全に読み込まれたときに実行
window.onload = () => {
    fetchAndDisplayIpAddress();
    fetchFileList();
};
Node.js
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const express = require('express');
const multer = require('multer');
const { app, BrowserWindow } = require('electron/main');

// Expressサーバーを初期化
const serverApp = express();

// JSONボディをパースするためのミドルウェアを追加
serverApp.use(express.json());

// アップロード先のディレクトリ
const uploadDir = path.join(__dirname, 'public', 'uploads');

// ディレクトリが存在しない場合は作成
fs.mkdir(uploadDir, { recursive: true }).catch(console.error);

// Multerの設定(ファイルの保存先とファイル名を指定)
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, uploadDir);
    },
    filename: (req, file, cb) => {
        cb(null, file.originalname);
    }
});

const upload = multer({ storage: storage });

// ローカルIPアドレスを取得する関数
function getLocalIpAddress() {
    const interfaces = os.networkInterfaces();
    for (const name of Object.keys(interfaces)) {
        for (const iface of interfaces[name]) {
            if ('IPv4' === iface.family && !iface.internal) {
                return iface.address;
            }
        }
    }
    return '127.0.0.1';
}

// === IPアドレスのエンドポイント(パスワード情報を削除)===
serverApp.get('/ip-address', (req, res) => {
    const ip = getLocalIpAddress();
    res.json({ ipAddress: ip }); // password: ACCESS_PASSWORD を削除
});


// アップロード、ファイル一覧、削除のエンドポイント
serverApp.post('/upload', upload.single('file'), (req, res) => {
    if (req.file) {
        res.status(200).send('File uploaded successfully');
    } else {
        res.status(400).send('No file uploaded');
    }
});

serverApp.get('/files', async (req, res) => {
    try {
        const files = await fs.readdir(uploadDir);
        res.json(files);
    } catch (error) {
        res.status(500).json({ error: 'Failed to retrieve file list' });
    }
});

serverApp.delete('/files/:filename', async (req, res) => {
    const filePath = path.join(uploadDir, req.params.filename);
    try {
        await fs.unlink(filePath);
        res.status(200).send('File deleted successfully');
    } catch (error) {
        console.error('Failed to delete file:', error);
        res.status(500).send('Failed to delete file');
    }
});

// === Electron アプリケーションのロジック ===
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600
  });

  // ExpressサーバーがリッスンするURLを読み込む
  win.loadURL(`http://localhost:3000`);
};

// Electronアプリが準備完了したらExpressサーバーとウィンドウを起動
app.whenReady().then(() => {
  const port = 3000;
  serverApp.listen(port, () => {
    console.log(`Server listening on port ${port}`);
  });
  createWindow();
});

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

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

パッケージ化手順

Mac、Windowsどちらの環境でもいいようにDockerで環境を構築します。

Dockerについてはこちらを参考にしてみてください。

前回の記事の通り進めれば問題ありません。

1.コンテナを作成

docker run -it -d --name tmp -p 8000:8000 ubuntu:22.04 

2.コンテナに入る

docker exec -it tmp /bin/bash

3.ubuntuのアップデート

apt update
apt upgrade

3.必要プログラムのインストール

apt install vim
apt install wget
apt install curl
apt install npm
apt install nodejs
npm install express
npm install multer --save

4.作業ディレクトリの作成と移動

cd home
mkdir app
cd app
mkdir public uploads assets
cd public

5.index.htmlの作成

vi index.html

エディターが立ち上がるので、コードのindex.htmlを貼り付けてください。

貼り付けたら、Ctrl + Cを押した後 :wq と入力してEnterキー押下で保存してください。

その後

6.script.jsの作成

vi  script.js

エディターが立ち上がるので、コードのscript.jsを貼り付けてください。

貼り付けたら、Ctrl + Cを押した後 :wq と入力してEnterキー押下で保存してください。

7.style.cssの作成

vi  style.css

エディターが立ち上がるので、コードのstyle.cssを貼り付けてください。

貼り付けたら、Ctrl + Cを押した後 :wq と入力してEnterキー押下で保存してください。

8.作業ディレクトリの移動

cd ../

9.index.jsの作成

vi  index.js

エディターが立ち上がるので、コードのindex.cssを貼り付けてください。

貼り付けたら、Ctrl + Cを押した後 :wq と入力してEnterキー押下で保存してください。

10.app-icon.pngとapp-icon.icnsの配置

アプリのアイコンです。AIに好きな画像を生成してもらってください。

app-icon.icnsはAIが作った画像を変換して作ってください。

方法はいくつかありますが、下記Webアプリを使うと簡単です。

11.下準備 

npm init -y

12.Node.jsのバージョンを上げつためのツールをインストール

バージョンを上げないと、Electronのインストールに失敗します。

# nvmのインストールスクリプトをダウンロードして実行
# 'curl' または 'wget' のどちらか一方でOKです。
# v0.40.3の部分は、最新のバージョンに置き換えてください。

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# 上記のcurlコマンドがうまくいかない場合や、wgetが好きな方はこちら

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# nvmの環境設定を現在のシェルに読み込む
# 通常、インストールスクリプトが~/.bashrcまたは~/.zshrcに設定を追記します。
source ~/.bashrc

↓反映が成功しているか確認↓

バージョンが表示されていれば成功です。

nvm --version

13.Node.jsのバージョンを上げる

nvm install 22

## ↓成功してるか確認できます。
node -v

14.ここでやっとElectronのインストール

npm install electron --save-dev

15.パッケージ化するためのツールをインストール

npm install @electron/packager --save-dev

16.コマンドの定義

vi package.json

↓ package.jsonのscriptの箇所に追記します↓

  "package:mac": "npx @electron/packager . FileStorage --platform=darwin --arch=universal --out=./build --overwrite --icon=./assets/app-icon.icns",
  "package:win": "npx @electron/packager . FileStorage --platform=win32 --arch=x64 --out=./build --overwrite --icon=./assets/app-icon.icns",
  "package:linux": "npx @electron/packager . FileStorage --platform=linux --arch=x64 --out=./build --overwrite --icon=./assets/app-icon.icns",
  "package:all": "npx @electron/packager . FileStorage --all --out=./build --overwrite",
  "package:mac-arm64": "npx @electron/packager . FileStorage --platform=darwin --arch=arm64 --out=./build --overwrite --icon=./assets/app-icon.icns"

貼り付けたら、Ctrl + Cを押した後 :wq と入力してEnterキー押下で保存してください。

17.うまく追記できたか確認

npm run

↓ 成功した時↓

18.パッケージ化のコマンド

コマンドが失敗する場合は、12.コマンドの定義を確認してみてください。

エラー文をそのままAIに読み込ませるのが手っ取り早いです。

✅AppleシリコンのMac用にビルドするコマンド

npm run package:mac-arm64


Wrote new app to:~と表示されたら成功です!

Windows用にビルドするコマンド

npm run package:win

↓パッケージ化されたアプリが出力される場所↓

app>build>FileStorage-darwin-arm64>FileStorage

アプリを開くをこのようなウィンドウが立ち上がるはずです!

ビルドにこのようなエラーが出た場合、、、

code: ‘ERR_MODULE_NOT_FOUND’, 
url: ‘file:///home/app/node_modules/@electron/packager/dist/cli.js’

下記コマンドを実行してみてください。

cd ./home/app

npm uninstall electron-packager electron/electron-packager
npm cache clean --force
rm -rf node_modules package-lock.json
npm install --save-dev @electron/packager
ls node_modules/@electron/packager/dist/cli.js

まとめ

ビルド方法さえ覚えてしまえば、それで終了です。

コードはAIが書いてくれるので、欲しいアプリを自作することは容易いですね😎

すげえ時代だ、、、

よかったらシェアしてね!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次