diff --git a/README.md b/README.md index 5476fbf..50eaf96 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,11 @@ https://github.com/MrMYHuang/cbetar2/releases/latest 11. 合成語音選項在Android Chrome無效。( https://stackoverflow.com/a/61366224/631869 ) ## 版本歷史 +* 2021.02.09: + * PWA 4.23.0: + * [新增] 支援檢查後端app下載進度顯示。 + * Backend 13.0.0: + * [新增] 支援檢查後端app更新、下載。 * PWA 4.22.0: * [新增] 經文頁開啟CBETA Online經文功能。 * PWA 4.21.5: diff --git a/electronBuilderConfigs/IsDeb.txt b/electronBuilderConfigs/IsDeb.txt new file mode 100644 index 0000000..e69de29 diff --git a/electronBuilderConfigs/IsMac.txt b/electronBuilderConfigs/IsMac.txt new file mode 100644 index 0000000..e69de29 diff --git a/electronBuilderConfigs/IsRpm.txt b/electronBuilderConfigs/IsRpm.txt new file mode 100644 index 0000000..e69de29 diff --git a/electronBuilderConfigs/IsWin.txt b/electronBuilderConfigs/IsWin.txt new file mode 100644 index 0000000..e69de29 diff --git a/electronBuilderConfigs/deb.json b/electronBuilderConfigs/deb.json index e630337..eb97b06 100644 --- a/electronBuilderConfigs/deb.json +++ b/electronBuilderConfigs/deb.json @@ -5,6 +5,13 @@ "buildElectron/**/*", "package.json" ], + "extraFiles": [ + { + "from": "electronBuilderConfigs", + "to": ".", + "filter": ["IsDeb.txt"] + } + ], "extraResources": [ "buildElectron/*.xsl" ], diff --git a/electronBuilderConfigs/mac.json b/electronBuilderConfigs/mac.json index 6e0c214..0c27a4f 100644 --- a/electronBuilderConfigs/mac.json +++ b/electronBuilderConfigs/mac.json @@ -6,6 +6,11 @@ "package.json" ], "extraResources": [ + { + "from": "electronBuilderConfigs", + "to": ".", + "filter": ["IsMac.txt"] + }, "buildElectron/*.xsl" ], "asarUnpack": [ diff --git a/electronBuilderConfigs/rpm.json b/electronBuilderConfigs/rpm.json index 6ccec75..63a24f9 100644 --- a/electronBuilderConfigs/rpm.json +++ b/electronBuilderConfigs/rpm.json @@ -5,6 +5,13 @@ "buildElectron/**/*", "package.json" ], + "extraFiles": [ + { + "from": "electronBuilderConfigs", + "to": ".", + "filter": ["IsRpm.txt"] + } + ], "extraResources": [ "buildElectron/*.xsl" ], diff --git a/electronBuilderConfigs/snap.json b/electronBuilderConfigs/snap.json index 56b27fd..cb79f86 100644 --- a/electronBuilderConfigs/snap.json +++ b/electronBuilderConfigs/snap.json @@ -6,7 +6,11 @@ "package.json" ], "extraFiles": [ - "electronBuilderConfigs/IsSnap.txt", + { + "from": "electronBuilderConfigs", + "to": ".", + "filter": ["IsSnap.txt"] + } ], "extraResources": [ "buildElectron/*.xsl" diff --git a/package-lock.json b/package-lock.json index d8c3acf..c991d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,12 @@ "source-map": "^0.5.0" }, "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -105,6 +111,14 @@ "@babel/helper-validator-option": "^7.12.1", "browserslist": "^4.14.5", "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "@babel/helper-create-class-features-plugin": { @@ -973,6 +987,14 @@ "@babel/helper-plugin-utils": "^7.10.4", "resolve": "^1.8.1", "semver": "^5.5.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "@babel/plugin-transform-shorthand-properties": { @@ -1123,6 +1145,14 @@ "@babel/types": "^7.12.11", "core-js-compat": "^3.8.0", "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "@babel/preset-modules": { @@ -3326,6 +3356,11 @@ "@types/node": "*" } }, + "@types/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -4312,7 +4347,6 @@ "version": "0.21.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "dev": true, "requires": { "follow-redirects": "^1.10.0" }, @@ -4320,8 +4354,7 @@ "follow-redirects": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", - "dev": true + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" } } }, @@ -4735,6 +4768,14 @@ "@babel/types": "^7.12.1", "core-js-compat": "^3.6.2", "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "@babel/preset-react": { @@ -6278,6 +6319,14 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "crypto-browserify": { @@ -9456,6 +9505,12 @@ "to-regex": "^3.0.2" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -14958,7 +15013,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -14993,6 +15047,12 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -15520,6 +15580,11 @@ "tslib": "^2.0.3" } }, + "node-downloader-helper": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-1.0.17.tgz", + "integrity": "sha512-EnaY0uBSdVo4kYfSmkDlTJG8GqmS8fbfoOau/OsTnikCwt9vsU0w8REVxwWbVz7DzNtHSEBKpU6jV1hmtlx0Dg==" + }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -15680,6 +15745,11 @@ "glob": "^7.1.3" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, "tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", @@ -15748,6 +15818,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "normalize-path": { @@ -18003,6 +18081,14 @@ "dev": true, "requires": { "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "react-dev-utils": { @@ -19400,9 +19486,12 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "requires": { + "lru-cache": "^6.0.0" + } }, "semver-compare": { "version": "1.0.0", @@ -23030,8 +23119,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.0", diff --git a/package.json b/package.json index 554c7a8..b4bb67d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "cbetar2", "productName": "電子佛典", - "pwaVersion": "4.22.0", - "version": "12.0.1", + "pwaVersion": "4.23.0", + "version": "13.0.0", "license": "MIT", "keywords": [ "CBETA", @@ -49,8 +49,12 @@ "extends": "react-app" }, "dependencies": { + "@types/semver": "^7.3.4", + "axios": "^0.21.1", "electron-window-state": "^5.0.3", - "libxslt-myh": "^0.9.7" + "libxslt-myh": "^0.9.7", + "node-downloader-helper": "^1.0.17", + "semver": "^7.3.4" }, "devDependencies": { "@ionic/react": "^5.5.3", @@ -68,7 +72,6 @@ "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.6", "@types/uuid": "^8.3.0", - "axios": "^0.21.1", "customize-cra": "^1.0.0", "electron": "^11.2.1", "electron-builder": "^22.9.1", diff --git a/src/App.tsx b/src/App.tsx index db82881..fa754af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,7 @@ import DictionaryPage from './pages/DictionaryPage'; import FullTextSearchPage from './pages/FullTextSearchPage'; import ShareTextModal from './components/ShareTextModal'; import WordDictionaryPage from './pages/WordDictionaryPage'; +import DownloadModal from './components/DownloadModal'; const electronBackendApi: any = (window as any).electronBackendApi; @@ -97,6 +98,7 @@ interface State { toastMessage: string; showUpdateAlert: boolean; showRestoreAppSettingsToast: boolean; + downloadModal: any; } class _App extends React.Component { @@ -135,6 +137,12 @@ class _AppOrig extends React.Component { val: data.isOn, }); break; + case 'DownloadingBackend': + this.setState({ downloadModal: { show: true, progress: data.progress / 100 } }); + break; + case 'DownloadingBackendDone': + this.setState({ downloadModal: { show: false, progress: this.state.downloadModal.progress } }); + break; } }); electronBackendApi?.send("toMain", { event: 'ready' }); @@ -183,6 +191,7 @@ class _AppOrig extends React.Component { showRestoreAppSettingsToast: (queryParams.settings != null && this.originalAppSettingsStr != null) || false, showToast: false, toastMessage: '', + downloadModal: { progress: 0, show: false } }; serviceWorkCallbacks.onUpdate = (registration: ServiceWorkerRegistration) => { @@ -385,6 +394,14 @@ class _AppOrig extends React.Component { }} /> + + { } + +interface State { + isAppSettingsExport: Array; +} + +class _DownloadModal extends React.Component { + + constructor(props: any) { + super(props); + this.state = { + isAppSettingsExport: Array(Object.keys(Globals.appSettings).length) + } + } + + render() { + return ( + + +
+
+ 後端app下載進度 +
+ + {Math.floor(this.props.progress * 100)}% +
+
+
+ ); + } +}; + +const mapStateToProps = (state: any /*, ownProps*/) => { + return { + } +}; + +//const mapDispatchToProps = {}; + +export default connect( + mapStateToProps, +)(_DownloadModal); diff --git a/srcElectron/Globals.ts b/srcElectron/Globals.ts index 24a84d6..146f23a 100644 --- a/srcElectron/Globals.ts +++ b/srcElectron/Globals.ts @@ -1 +1,11 @@ -export const localFileProtocolName = 'safe-file-protocol'; \ No newline at end of file +export const localFileProtocolName = 'safe-file-protocol'; +export const latestDownloadUrl = 'https://github.com/MrMYHuang/cbetar2/releases/latest/download'; +import * as fs from 'fs'; +export function backendAppPackageType() { + if (fs.existsSync(`${process.resourcesPath}/IsWin.txt`)) return 'win'; + if (fs.existsSync(`${process.resourcesPath}/IsMac.txt`)) return 'mac'; + if (fs.existsSync(`${process.resourcesPath}/IsRpm.txt`)) return 'rpm'; + if (fs.existsSync(`${process.resourcesPath}/IsDeb.txt`)) return 'deb'; + if (fs.existsSync(`${process.resourcesPath}/IsSnap.txt`)) return 'snap'; + return 'unknown'; +} \ No newline at end of file diff --git a/srcElectron/Update.ts b/srcElectron/Update.ts new file mode 100644 index 0000000..09db5ac --- /dev/null +++ b/srcElectron/Update.ts @@ -0,0 +1,76 @@ +import * as semver from 'semver'; +import { dialog, BrowserWindow } from 'electron'; +import axios from 'axios'; +import { DownloaderHelper, Stats } from 'node-downloader-helper'; +import * as Globals from './Globals'; +const PackageInfos = require('../package.json'); + +const axiosInstance = axios.create({ + timeout: 5000, +}); + +export async function lookupLatestVersion() { + const result = await axiosInstance.get(`${Globals.latestDownloadUrl}/latest.yml`, { responseType: 'text' }); + return /version: (.*)\n/.exec(result.data)![1]; +} + +export async function check(browserWindow: BrowserWindow) { + const lastestVersion = await lookupLatestVersion(); + if (semver.gte(PackageInfos.version, lastestVersion)) { + dialog.showMessageBox({ + type: 'info', + message: '後端app已是最新版!' + }); + return + } + + const clickedButtonId = dialog.showMessageBoxSync(browserWindow, { + type: 'question', + message: `發現新版cbetar2 ${lastestVersion}後端app,是否下載安裝檔?`, + buttons: ['取消', '下載'], + }); + + let packageSuffix = 'win64.exe'; + switch (Globals.backendAppPackageType()) { + case 'win': packageSuffix = 'win64.exe'; break; + case 'mac': packageSuffix = 'macos64.pkg'; break; + case 'rpm': packageSuffix = 'linux64.rpm'; break; + case 'deb': packageSuffix = 'linux64.deb'; break; + case 'snap': packageSuffix = 'linux64.snap'; break; + } + + if (clickedButtonId) { + const path = dialog.showOpenDialogSync(browserWindow, { + message: '選擇下載位置', + properties: ['openDirectory'] + }); + const file = `${PackageInfos.name}_${lastestVersion}_${packageSuffix}`; + const downloadUrl = `${Globals.latestDownloadUrl}/${file}`; + + if (path) { + const dl = new DownloaderHelper(downloadUrl, path[0]); + browserWindow.webContents.send('fromMain', { event: 'DownloadingBackend', progress: 0 }); + let progressUpdateEnable = true; + dl.on('progress', (stats: Stats) => { + if (progressUpdateEnable) { + // Reduce number of this calls. + // Too many of this calls could result in 'end' event callback is executed before 'progress' event callbacks! + browserWindow.webContents.send('fromMain', { event: 'DownloadingBackend', progress: stats.progress }); + progressUpdateEnable = false; + setTimeout(() => { + progressUpdateEnable = true; + }, 100); + } + }); + dl.on('end', (downloadInfo: any) => { + dl.removeAllListeners(); + browserWindow.webContents.send('fromMain', { event: 'DownloadingBackendDone' }); + dialog.showMessageBox({ + type: 'info', + message: '新版後端app安裝程式下載完成!請關閉app,手動執行安裝程式。' + }); + }); + dl.start(); + } + } +} \ No newline at end of file diff --git a/srcElectron/main.ts b/srcElectron/main.ts index a2ee991..ffaeb14 100644 --- a/srcElectron/main.ts +++ b/srcElectron/main.ts @@ -5,6 +5,7 @@ const path = require('path'); import * as fs from 'fs'; import * as os from 'os'; const PackageInfos = require('../package.json'); +import * as update from './Update'; import * as cbetaOfflineDb from './CbetaOfflineDb'; import * as Globals from './Globals'; @@ -55,6 +56,7 @@ async function setCbetaBookcase() { } } else { dialog.showMessageBox({ + type: 'info', message: '設定取消' }); } @@ -79,6 +81,13 @@ function loadSettings() { } } +async function checkUpdate() { + const latestVersion = await update.lookupLatestVersion(); + update.check(mainWindow!); + settings.lastCheckedVersion = latestVersion; + fs.writeFileSync(backendAppSettingsFile, JSON.stringify(settings)); +} + const template = [ new MenuItem({ label: '檔案', @@ -142,13 +151,17 @@ const template = [ role: 'forceReload', label: '強制重新載入', }, + { + label: '檢查後端app更新', + click: checkUpdate, + }, ] }), ]; const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) -function createWindow() { +async function createWindow() { frontendIsReady = false; let mainWindowState = windowStateKeeper({ @@ -173,11 +186,16 @@ function createWindow() { // Open the DevTools. //mainWindow.webContents.openDevTools() - ipcMain.on('toMain', (ev, args) => { + ipcMain.on('toMain', async (ev, args) => { switch (args.event) { case 'ready': frontendIsReady = true; loadSettings(); + const latestVersion = await update.lookupLatestVersion(); + // Ask for updating for each new version once. + if (settings.lastCheckedVersion !== latestVersion) { + checkUpdate(); + } mainWindow?.webContents.send('fromMain', { event: 'version', version: PackageInfos.version }); break; case 'fetchCatalog':