electron实现本地音乐播放器

本项目构建环境:

node => v12.15.0
electron => 10.1.3

搭建环境:

# 克隆示例项目的仓库
$ git clone https://github.com/electron/electron-quick-start

# 进入这个仓库
$ cd electron-quick-start

# 安装依赖并运行
$ npm install && npm start

本项目所需依赖:

"devDependencies": {
  "electron": "^10.1.3",
  "electron-builder": "^22.8.1"
},
"dependencies": {
  "bootstrap": "^4.5.2",
  "electron-store": "^6.0.1",
  "nodemon": "^2.0.4",
  "uuid": "^8.3.1"
}

本项目无使用任何前端框架,纯原生。(体验一把原生开发带来的快落吧)
在这里插入图片描述

修改 main.js

const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const DataStore = require('./renderer/MusicDataStore')

const myStore = new DataStore({ name: 'Music Data' })

// 封装一个 BrowserWindow,避免每一次重复的编写
class AppWindow extends BrowserWindow {
  constructor(config, fileLocation) {
    const baseConfig = {
      width: 960,
      height: 660,
      webPreferences: {
        nodeIntegration: true
      }
    }
    // 两个对象合并
    const finalConfig = { ...baseConfig, ...config }
    super(finalConfig)
    this.loadFile(fileLocation)
    this.once('ready-to-show', () => {
      this.show()
    })
  }
}

app.on('ready', () => {
    // 加载主页面
  const mainWindow = new AppWindow({}, './renderer/index.html')
  mainWindow.webContents.on('did-finish-load', () => {
    mainWindow.send('getTracks', myStore.getTracks())
  })
  ipcMain.on('add-music-window', () => {
    const addWindow = new AppWindow({
      width: 800,
      height: 600,
      // 指定主进程
      parent: mainWindow
    }, './renderer/add.html')
  })
  // 添加数据
  ipcMain.on('add-tracks', (event, tracks) => {
    const updatedTracks = myStore.addTracks(tracks).getTracks()
    mainWindow.send('getTracks', updatedTracks)
  })
  // 删除数据
  ipcMain.on('delete-track', (event, id) => {
    const updatedTracks = myStore.deleteTrack(id).getTracks()
    mainWindow.send('getTracks', updatedTracks)
  })
  ipcMain.on('open-music-file', (event) => {
    dialog.showOpenDialog({
      properties: ['openFile', 'multiSelections'],
      filters: [{ name: 'Music', extensions: ['mp3'] }]
    }).then((files) => {
      if (files) {
        event.sender.send('selected-file', files)
      }
    })
  })
})

index.html
(这是我们的主页面)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>本地播放器</title>
    <link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="./index.css">
    <link rel="stylesheet" href="../font/iconfont.css">
  </head>
  <body>
    <div class="music-content">
      <div class="container">
        <h1>我的播放器</h1>
        <button 
          type="button" 
          class="btn btn-primary btn-lg btn-block mt-4"
          id="add-music-button"
        >
          添加歌曲到曲库
        </button>
        <div id="tracksList" class="mt-4">
        </div>
        <div class="container fixed-bottom pb-4 ">
          <hr/>
          <div class="row my-2" id="player-status"></div>
          <div class="progress">
            <div class="progress-bar" id="player-progress" role="progressbar" style="width: 0%;">
              0%
            </div>
          </div>
        </div>
      </div>
    </div>
    <script>
      require('./index.js');
    </script>
  </body>
</html>

index.js

const { ipcRenderer } = require('electron')
const { $, convertDuration, str } = require('./helper')
let musicAudio = new Audio()
let allTracks
let currentTrack

$('add-music-button').addEventListener('click', () => {
  ipcRenderer.send('add-music-window')
})

const renderListHTML = (tracks) => {
  const tracksList = $('tracksList')
  const tracksListHTML = tracks.reduce((html, track) => {
    html += `<li class="row music-track list-group-item d-flex justify-content-between align-items-center">
      <div class="col-10">
        <i class="mr-2 text-secondary iconfont icon-yinle"></i>
        <b>${str(track.fileName)}</b>
      </div>
      <div class="col-2">
        <i class="fas fa-play mr-3 icon-ziyuan iconfont" data-id="${track.id}"></i>
        <i class="fas fa-trash-alt icon-del iconfont" data-id="${track.id}"></i>
      </div>
    </li>`
    return html
  }, '')
  const emptyTrackHTML = '<div class="alert alert-primary">还没有添加任何音乐</div>'
  tracksList.innerHTML = tracks.length ? `<ul class="list-group">${tracksListHTML}</ul>` : emptyTrackHTML
}

const renderPlayerHTML = (name, duration) => {
  const player = $('player-status')
  const html = `<div class="col font-weight-bold"">
        正在播放:${str(name)}
      </div>
  <div class="col">
    <span id="current-seeker">00:00</span> / ${convertDuration(duration)}
  </div>`
  player.innerHTML = html
}

const updateProgressHTML = (currentTime, duration) => {
  // 计算 progress 的数值
  const progress = Math.floor(currentTime / duration * 100)
  const bar = $('player-progress')
  bar.innerHTML = progress + '%'
  bar.style.width = progress + '%'
  const seeker = $('current-seeker')
  seeker.innerHTML = convertDuration(currentTime)
}

ipcRenderer.on('getTracks', (event, tracks) => {
  allTracks = tracks
  renderListHTML(tracks)
})

musicAudio.addEventListener('loadedmetadata', () => {
  // 开始渲染播放器状态
  renderPlayerHTML(currentTrack.fileName, musicAudio.duration)
})

musicAudio.addEventListener('timeupdate', () => {
  // 更新播放器状态
  updateProgressHTML(musicAudio.currentTime, musicAudio.duration)
})

$('tracksList').addEventListener('click', (event) => {
  event.preventDefault()
  const { dataset, classList } = event.target
  const id = dataset && dataset.id
  if (id && classList.contains('icon-ziyuan')) {
    // 播放音乐
    if (currentTrack && currentTrack.id === id) {
      // 继续播放音乐
      musicAudio.play()
    } else {
      // 播放新的歌曲
      currentTrack = allTracks.find(track => track.id === id)
      musicAudio.src = currentTrack.path
      musicAudio.play()
      const resetIconEle = document.querySelector('.icon-zanting')
      if (resetIconEle) {
          // 更改播放图标
        resetIconEle.classList.replace('icon-zanting', 'icon-ziyuan')
      }
    }
    classList.replace('icon-ziyuan', 'icon-zanting')
  } else if (id && classList.contains('icon-zanting')) {
    // 暂停
    musicAudio.pause()
    classList.replace('icon-zanting', 'icon-ziyuan')
  } else if (id && classList.contains('icon-del')) {
    // 删除
    ipcRenderer.send('delete-track', id)
  }
})

MusicDataStore.js
(这是存储音乐数据的地方)

const Store = require('electron-store')
const {path} = require('path')
// 保证唯一的ID
const { v4: uuidv4 } = require('uuid');

class DataStore extends Store {
  constructor(settings) {
    super(settings);
    this.tracks = this.get('tracks') || []
  }
  // 保存数据
  saveTracks() {
    this.set('tracks', this.tracks)
    return this
  }
  // 获取数据
  getTracks() {
    return this.get('tracks') || []
  }
  // 添加数据
  addTracks(tracks) {
    const tracksWithProps = tracks.map(track => {
      return {
        id: uuidv4(),
        path: track,
        fileName: track
      }
    }).filter(track => {
      const currentTrackPath = this.getTracks().map(track => track.path)
      return currentTrackPath.indexOf(track.path) < 0
    })
    this.tracks = [...this.tracks, ...tracksWithProps]
    return this.saveTracks()
  }
  // 删除数据
  deleteTrack(deleteId) {
    this.tracks = this.tracks.filter(item => item.id !== deleteId)
    return this.saveTracks()
  }
}

module.exports = DataStore

大概的主代码就差不多了。剩下的一些辅助代码我就不写出来了。

(这个 demo 其实是存在 bug,就是显示音乐的名字有一点问题)
在这里插入图片描述

我在 electron 引入 path 模块是 undefined,这个我不知道是为什么,所以代码的名字我只是做了很简单的处理。就很丑了。

代码地址奉上
https://gitee.com/suiboyu/electron-music

下载之后的代码中 electron-music/outName/appName-win32-x64,中有一个 appName.exe 就是打包出来的 exe(这个打包好像也行有点问题的,不过 demo 嘛,刚刚开始学习,凑合凑合用吧)

在这里插入图片描述

在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值