Commit ef06d662 authored by 神楽坂玲奈's avatar 神楽坂玲奈

most works

parent 9d58e148
......@@ -43,3 +43,4 @@ testem.log
# System Files
.DS_Store
Thumbs.db
/bin/
......@@ -38,6 +38,10 @@
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"allowedCommonJsDependencies": [
"glob",
"aria2"
],
"customWebpackConfig": {},
"outputPath": "dist/mycard",
"index": "src/index.html",
......
require('@electron/remote/main').initialize()
'use strict';
// 处理提权
function handleElevate() {
// for debug
if (process.argv[1] === '.') {
process.argv[1] = process.argv[2];
process.argv[2] = process.argv[3];
}
if (process.argv[1] === '-e') {
if (process.platform === 'darwin') {
require('electron').app.dock.hide();
}
let elevate = JSON.parse(Buffer.from(process.argv[2], 'base64').toString());
require('net').connect(elevate['ipc'], function() {
process.send = (message, sendHandle, options, callback) => this.write(JSON.stringify(message) + require('os').EOL, callback);
this.on('end', () => process.emit('disconnect'));
require('readline').createInterface({ input: this }).on('line', (line) => process.emit('message', JSON.parse(line)));
process.argv = elevate['arguments'][1];
require('./' + elevate['arguments'][0]);
});
return true;
}
}
if (handleElevate()) {
return;
}
const { ipcMain, app, shell, BrowserWindow, Menu, Tray, Notification } = require('electron');
const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev');
const child_process = require('child_process');
const path = require('path');
require('@electron/remote/main').initialize();
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
// aria2c启动失败则为null
let aria2c;
// 单实例
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
}
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus();
}
});
// 调试模式
if (!process.env['NODE_ENV']) {
process.env['NODE_ENV'] = isDev ? 'development' : 'production';
}
// 自动更新
let updateWindow;
global.autoUpdater = autoUpdater;
// if (process.env['NODE_ENV'] == 'production' && process.platform == 'darwin') {
// autoUpdater.setFeedURL("https://wudizhanche.mycard.moe/update/darwin/" + app.getVersion());
// }
// else{
// setTimeout(()=>{
// autoUpdater.emit('checking-for-update')
// }, 5000)
// setTimeout(()=>{
// autoUpdater.emit('error', '1')
// }, 6000)
// }
autoUpdater.on('error', (event) => {
global.update_status = 'error';
console.log('autoUpdater', 'error', event);
......@@ -23,69 +100,180 @@ autoUpdater.on('update-not-available', () => {
autoUpdater.on('update-downloaded', (event) => {
global.update_status = 'update-downloaded';
console.log('autoUpdater', 'update-downloaded', event);
// updateWindow = new BrowserWindow({
// width: 640,
// height: 360,
// });
// updateWindow.loadURL(`file://${__dirname}/update.html`);
// updateWindow.webContents.on('new-window', function (e, url) {
// e.preventDefault();
// shell.openExternal(url);
// });
// updateWindow.on('closed', function () {
// updateWindow = null;
// });
// ipcMain.on('update', (event, arg) => {
// autoUpdater.quitAndInstall();
// });
updateWindow = new BrowserWindow({
width: 640,
height: 360
});
updateWindow.loadURL(`file://${__dirname}/update.html`);
updateWindow.webContents.on('new-window', function(e, url) {
e.preventDefault();
shell.openExternal(url);
});
updateWindow.on('closed', function() {
updateWindow = null;
});
ipcMain.on('update', (event, arg) => {
autoUpdater.quitAndInstall();
});
});
// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron');
const path = require('path');
// Aria2c
function createAria2c() {
let aria2c_path;
switch (process.platform) {
case 'win32':
if (process.env['NODE_ENV'] === 'production') {
aria2c_path = path.join(process.resourcesPath, 'bin', 'aria2c.exe');
} else {
aria2c_path = path.join('bin', 'aria2c.exe');
}
break;
case 'darwin':
if (process.env['NODE_ENV'] === 'production') {
aria2c_path = path.join(process.resourcesPath, 'bin', 'aria2c');
} else {
aria2c_path = path.join('bin', 'aria2c');
}
break;
case 'linux':
aria2c_path = '/usr/bin/aria2c';
break;
default:
throw 'unsupported platform';
}
aria2c = child_process.spawn(aria2c_path, [
'--enable-rpc',
'--rpc-allow-origin-all',
'--rpc-listen-port=6800',
'--continue',
'--split=10',
'--min-split-size=1M',
'--max-connection-per-server=10',
'--remove-control-file',
'--allow-overwrite'
], { stdio: 'ignore' });
}
// 主窗口
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
mainWindow = new BrowserWindow({
width: 1024,
height: 640,
minWidth: 1024,
minHeight: 640,
frame: process.platform === 'darwin',
titleBarStyle: process.platform === 'darwin' ? 'hidden' : undefined,
webPreferences: { nodeIntegration: true, contextIsolation: false, enableRemoteModule: true, webviewTag: true },
// transparent: process.platform != 'darwin',
titleBarStyle: process.platform === 'darwin' ? 'hidden' : undefined
});
// and load the index.html of the app.
if (process['NODE_ENV'] !== 'production') {
mainWindow.loadURL('http://localhost:4200');
if (isDev) {
mainWindow.loadURL('http://localhost:4200/');
} else {
mainWindow.loadFile('dist/ci/index.html');
mainWindow.loadFile('dist/mycard/index.html');
}
mainWindow.webContents.on('new-window', function(e, url) {
e.preventDefault();
shell.openExternal(url);
});
// Open the DevTools.
mainWindow.webContents.openDevTools()
if (process.env['NODE_ENV'] === 'development') {
mainWindow.webContents.openDevTools();
}
// Emitted when the window is closed.
mainWindow.on('closed', function() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
}
let tray;
function createTray() {
tray = new Tray(path.join(__dirname, 'images', 'icon.ico'));
tray.on('click', (event) => {
console.log(event);
if (event.metaKey) {
mainWindow.webContents.openDevTools();
} else {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
}
});
const contextMenu = Menu.buildFromTemplate([
// {label: '游戏', type: 'normal', click: (menuItem, browserWindow, event)=>{}},
// {label: '社区', type: 'normal', click: (menuItem, browserWindow, event)=>{}},
// {label: '切换账号', type: 'normal', click: (menuItem, browserWindow, event)=>{}},
{
label: '显示主界面', type: 'normal', click: () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
}
},
{
label: '退出', type: 'normal', click: app.quit
}
]);
tray.setToolTip('MyCard');
tray.setContextMenu(contextMenu);
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
createWindow();
app.on('activate', function() {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
if (process.platform === 'win32') {
createTray();
}
createAria2c();
aria2c.on('error', (err) => {
new Notification({ title: 'MyCard', body: '启动aria2失败,可能是被杀毒软件误删。游戏下载和更新功能将不可用。' }).show();
console.error(err);
aria2c = null;
});
if (process.env['NODE_ENV'] === 'production') {
/*let updateTempPath = '~/.cache/mycard-updater'
if (process.platform === 'win32') {
updateTempPath = `${process.env.LOCALAPPDATA}\\mycard-updater`
} else if (process.platform === 'darwin') {
updateTempPath = '~/Library/Application Support/Caches/mycard-updater'
}
try {
await require('fs').promises.mkdir(updateTempPath, {recursive: true});
} catch(e) {}*/
await autoUpdater.checkForUpdates();
}
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
// Quit when all windows are closed.
app.on('window-all-closed', function() {
if (process.platform !== 'darwin') app.quit();
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', function() {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. 也可以拆分成几个文件,然后用 require 导入。
// code. You can also put them in separate files and require them here.
app.on('quit', () => {
// windows 在非 detach 模式下会自动退出子进程
if (process.platform !== 'win32' && aria2c) {
aria2c.kill();
}
});
......@@ -10,7 +10,16 @@
"dependencies": {
"@electron/remote": "^1.2.1",
"@fortawesome/fontawesome-free": "^5.15.4",
"electron-updater": "^4.3.9"
"@types/ini": "^1.3.30",
"@types/jquery": "^3.5.6",
"@types/mustache": "^4.1.2",
"aria2": "^3.0.1",
"electron-is-dev": "^2.0.0",
"electron-updater": "^4.3.9",
"ini": "^2.0.0",
"jquery": "^3.6.0",
"mustache": "^4.2.0",
"reconnecting-websocket": "^4.4.0"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^12.1.1",
......@@ -3261,10 +3270,15 @@
"@types/node": "*"
}
},
"node_modules/@types/ini": {
"version": "1.3.30",
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.30.tgz",
"integrity": "sha512-2+iF8zPSbpU83UKE+PNd4r/MhwNAdyGpk3H+VMgEH3EhjFZq1kouLgRoZrmIcmoGX97xFvqdS44DkICR5Nz3tQ=="
},
"node_modules/@types/jquery": {
"version": "3.5.6",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.6.tgz",
"integrity": "sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==",
"dev": true,
"dependencies": {
"@types/sizzle": "*"
}
......@@ -3294,6 +3308,11 @@
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"dev": true
},
"node_modules/@types/mustache": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.1.2.tgz",
"integrity": "sha512-c4OVMMcyodKQ9dpwBwh3ofK9P6U9ZktKU9S+p33UqwMNN1vlv2P0zJZUScTshnx7OEoIIRcCFNQ904sYxZz8kg=="
},
"node_modules/@types/node": {
"version": "16.7.10",
"integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==",
......@@ -3311,8 +3330,7 @@
},
"node_modules/@types/sizzle": {
"version": "2.3.3",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ=="
},
"node_modules/@types/source-list-map": {
"version": "0.1.2",
......@@ -3840,6 +3858,33 @@
"version": "2.0.1",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/aria2": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/aria2/-/aria2-3.0.1.tgz",
"integrity": "sha512-aPB6KAq8UEHhJj5Yxs1Dg9670xYJoNAAB82S994xpUM2lnkJ5BqGAvK3Ladl58WikGcoBAC8P0yvxm6yrUSy4w==",
"dependencies": {
"commander": "^2.9.0",
"node-fetch": "^2.1.2",
"polygoat": "^1.1.4",
"ws": "^5.1.1"
},
"bin": {
"aria2rpc": "bin/cli.js"
}
},
"node_modules/aria2/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/aria2/node_modules/ws": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz",
"integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==",
"dependencies": {
"async-limiter": "~1.0.0"
}
},
"node_modules/arr-diff": {
"version": "4.0.0",
"integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
......@@ -3957,8 +4002,7 @@
},
"node_modules/async-limiter": {
"version": "1.0.1",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"dev": true
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"node_modules/asynckit": {
"version": "0.4.0",
......@@ -6797,6 +6841,14 @@
"node": ">= 10.0.0"
}
},
"node_modules/electron-is-dev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz",
"integrity": "sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-publish": {
"version": "22.11.7",
"integrity": "sha512-A4EhRRNBVz4SPzUlBrPO6BmuyDeI0pyprggPAV9rQ+SDVSnSB/WKPot9JwWMyArkGj3AUUTMNVT6hwZhMvhfqw==",
......@@ -8737,8 +8789,8 @@
},
"node_modules/ini": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
"dev": true,
"engines": {
"node": ">=10"
}
......@@ -9349,6 +9401,11 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
......@@ -10305,6 +10362,14 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/mute-stream": {
"version": "0.0.8",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
......@@ -10398,6 +10463,14 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz",
"integrity": "sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==",
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/node-forge": {
"version": "0.10.0",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
......@@ -11297,6 +11370,11 @@
"node": ">=8"
}
},
"node_modules/polygoat": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/polygoat/-/polygoat-1.1.4.tgz",
"integrity": "sha1-Mp+aDRstSkUUniU5Ujxvff2FCl8="
},
"node_modules/portfinder": {
"version": "1.0.28",
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
......@@ -13741,6 +13819,11 @@
"node": ">=8.10.0"
}
},
"node_modules/reconnecting-websocket": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz",
"integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng=="
},
"node_modules/reflect-metadata": {
"version": "0.1.13",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
......@@ -19612,10 +19695,15 @@
"@types/node": "*"
}
},
"@types/ini": {
"version": "1.3.30",
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.30.tgz",
"integrity": "sha512-2+iF8zPSbpU83UKE+PNd4r/MhwNAdyGpk3H+VMgEH3EhjFZq1kouLgRoZrmIcmoGX97xFvqdS44DkICR5Nz3tQ=="
},
"@types/jquery": {
"version": "3.5.6",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.6.tgz",
"integrity": "sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
......@@ -19645,6 +19733,11 @@
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"dev": true
},
"@types/mustache": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.1.2.tgz",
"integrity": "sha512-c4OVMMcyodKQ9dpwBwh3ofK9P6U9ZktKU9S+p33UqwMNN1vlv2P0zJZUScTshnx7OEoIIRcCFNQ904sYxZz8kg=="
},
"@types/node": {
"version": "16.7.10",
"integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==",
......@@ -19662,8 +19755,7 @@
},
"@types/sizzle": {
"version": "2.3.3",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ=="
},
"@types/source-list-map": {
"version": "0.1.2",
......@@ -20110,6 +20202,32 @@
"version": "2.0.1",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"aria2": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/aria2/-/aria2-3.0.1.tgz",
"integrity": "sha512-aPB6KAq8UEHhJj5Yxs1Dg9670xYJoNAAB82S994xpUM2lnkJ5BqGAvK3Ladl58WikGcoBAC8P0yvxm6yrUSy4w==",
"requires": {
"commander": "^2.9.0",
"node-fetch": "^2.1.2",
"polygoat": "^1.1.4",
"ws": "^5.1.1"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"ws": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz",
"integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==",
"requires": {
"async-limiter": "~1.0.0"
}
}
}
},
"arr-diff": {
"version": "4.0.0",
"integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
......@@ -20192,8 +20310,7 @@
},
"async-limiter": {
"version": "1.0.1",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"dev": true
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"asynckit": {
"version": "0.4.0",
......@@ -22320,6 +22437,11 @@
}
}
},
"electron-is-dev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz",
"integrity": "sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA=="
},
"electron-publish": {
"version": "22.11.7",
"integrity": "sha512-A4EhRRNBVz4SPzUlBrPO6BmuyDeI0pyprggPAV9rQ+SDVSnSB/WKPot9JwWMyArkGj3AUUTMNVT6hwZhMvhfqw==",
......@@ -23821,8 +23943,8 @@
},
"ini": {
"version": "2.0.0",
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
"dev": true
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="
},
"inquirer": {
"version": "8.1.1",
......@@ -24252,6 +24374,11 @@
}
}
},
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"js-tokens": {
"version": "4.0.0",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
......@@ -24962,6 +25089,11 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="
},
"mute-stream": {
"version": "0.0.8",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
......@@ -25036,6 +25168,11 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-fetch": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz",
"integrity": "sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA=="
},
"node-forge": {
"version": "0.10.0",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
......@@ -25697,6 +25834,11 @@
"find-up": "^4.0.0"
}
},
"polygoat": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/polygoat/-/polygoat-1.1.4.tgz",
"integrity": "sha1-Mp+aDRstSkUUniU5Ujxvff2FCl8="
},
"portfinder": {
"version": "1.0.28",
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
......@@ -27410,6 +27552,11 @@
"picomatch": "^2.2.1"
}
},
"reconnecting-websocket": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz",
"integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng=="
},
"reflect-metadata": {
"version": "0.1.13",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
......
:host {
flex-grow: 1;
position: relative;
padding: 1rem 1rem 0 1rem;
background-blend-mode: color;
background-size: 100% auto !important;
background-repeat: no-repeat !important;
}
.list-group {
width: 20rem;
}
progress {
margin: 2px 0 0;
}
.carousel-inner img {
width: 100%;
}
.dependency {
margin-right: 0.8em;
}
#news p {
margin-bottom: 0;
}
#news a {
display: block;
}
#network {
display: inline-block;
vertical-align: middle;
width: 230px;
}
#network .input-group-btn > .btn:not(:last-child):not(.dropdown-toggle) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
#network .input-group-btn > .dropdown-toggle {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.i-b{
display: inline-block;
}
.custom-file {
width: 100%;
}
.custom-file-control:lang(en)::after {
content: initial;
}
.custom-file-control {
overflow: hidden;
white-space: nowrap;
}
h1 {
font-size: 28px;
}
#status {
font-size: 15px;
}
h2 {
font-size: 20px;
margin-bottom: 0;
}
.cover {
width: 128px;
height: 128px;
object-fit: contain;
box-shadow: 0 0 4px #ccc;
}
.banner {
width: 120px;
height: 45px;
object-fit: cover;
}
#main {
display: flex;
flex-direction: row;
}
.panel {
border: 1px solid #eceeef;
border-radius: 6px;
background: rgba(255, 255, 255, .7);
padding: .8rem;
margin-bottom: 1rem;
box-shadow: 0 0 15px rgba(0, 0, 0, .05);
position: relative;
}
#news h3 > .title {
font-size: 1rem;
color: inherit;
}
#news h3 {
padding-top: .8rem;
margin-bottom: 0;
}
#news p {
font-size: 14px;
color: #888;
}
#news a {
font-size: 14px;
color: #00a4d9;
}
#news span {
font-size: 12px;
color: #ccc;
}
.moreinfo {
color: #00a4d9;
display: block;
position: absolute;
top: 12px;
right: 18px;
font-size: 14px;
}
#local h2 {
margin-bottom: .8rem;
}
#main {
display: flex;
flex-direction: row;
}
#right {
margin-left: 1rem;
}
h1 {
font-size: 28px;
margin-bottom: 0;
}
#time {
font-size: 14px;
margin-bottom: .6rem;
visibility: hidden;
}
th {
width: 25%;
}
.moreinfo {
color: #00a4d9;
display: block;
position: absolute;
top: 12px;
right: 18px;
font-size: 14px;
}
#arena {
position: relative;
}
.btn-primary {
background-color: #00a4d9;
border-color: #008dbb;
}
/* 竞技场 */
h2 {
font-size: 20px;
}
dt, dd {
font-size: 14px;
}
table {
margin-top: .5rem;
margin-bottom: 0;
}
table th, table td {
border-top: none;
font-size: 14px;
font-weight: normal;
}
#game_info {
font-size: 14px;
margin-right: 8px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
#game_info p {
flex-grow: 1;
}
#game_info_2 {
width: 160px;
flex-shrink: 0;
}
.tag {
font-size: 12px;
padding: 2px 5px;
margin-right: 5px;
}
table.expansions thead {
color: #ffffff;
background-color: #5bc0de;
}
table.expansions th {
width: auto;
vertical-align: middle;
}
table.expansions td {
line-height: 220%;
vertical-align: middle;
}
table.expansions thead th:first-child{
border-top-left-radius: 5px;
width: 5%;
}
table.expansions thead th:last-child{
border-top-right-radius: 5px;
width: 20%;
}
table.expansions tr:last-child td:first-child{
border-bottom-left-radius: 5px;
}
table.expansions tr:last-child td:last-child{
border-bottom-right-radius: 5px;
}
#purchase-form .form-check {
padding-right: 8px;
}
#purchase-form legend {
font-size: 1rem;
margin-bottom: 0;
margin-top: .5rem;
}
<div id="main" class="panel">
<img *ngIf="currentApp.cover" class="cover rounded" [src]="currentApp.cover">
<div id="right">
<h1>{{currentApp.name}}</h1>
<!-- <div id="time">您已玩了 2564 小时</div>-->
<!--应用未购买-->
<div *ngIf="!currentApp.isBought()">
<button i18n type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#purchase-modal">{{currentApp.price.cny | currency:'CNY':true}} 购买</button>
<button i18n type="button" class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#install-modal">安装试玩版</button>
<!--<button i18n (click)="updateInstallOption(currentApp)" type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#install-modal">我已经购买过</button>-->
</div>
<!--应用已购买,未安装-->
<div *ngIf="currentApp.isBought() && !currentApp.isInstalled()" class="i-b">
<!-- Button trigger modal -->
<button i18n type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#install-modal">安装</button>
<button i18n *ngIf="currentApp.runnable()" type="button" class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#import-modal">导入</button>
</div>
<!--应用变更中-->
<div *ngIf="currentApp.isInstalled() && !currentApp.isReady()" class="i-b">
<div id="status" class="i-b">
<span i18n *ngIf="currentApp.isDownloading()">正在下载</span>
<span i18n *ngIf="currentApp.isInstalling()">正在安装...</span>
<span i18n *ngIf="currentApp.isUninstalling()">正在卸载...</span>
<span i18n *ngIf="currentApp.isWaiting()">等待安装...</span>
<span i18n *ngIf="currentApp.isUpdating()">正在更新...</span>
<span *ngIf="currentApp.status.total">{{(currentApp.status.progress/currentApp.status.total * 100).toFixed()}}%</span>
<span>{{currentApp.progressMessage()}}</span>
</div>
<progress class="progress" [class.progress-striped]="!currentApp.status.total" [class.progress-animated]="!currentApp.status.total" value="{{currentApp.status.total ? currentApp.status.progress : 1}}" max="{{currentApp.status.total}}"></progress>
</div>
<!--应用ready-->
<div *ngIf="currentApp.isReady() && !currentApp.isYGOPro" class="i-b">
<button *ngIf="currentApp.runnable()" (click)="runApp(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-primary btn-sm">
<i class="fa fa-play" aria-hidden="true"></i> <span i18n>运行</span></button>
<button *ngIf="currentApp.actions.get('network')" [disabled]="!appsService.allReady(currentApp)" (click)="runApp(currentApp,'network')" type="button" class="btn btn-primary btn-sm">
<i class="fa fa-play" aria-hidden="true"></i> <span>运行 (联机版)</span></button>
<button *ngIf="currentApp.runnable() && currentApp.id === 'th123' && currentApp.references.get('sokuroll')!.isReady()" (click)="runRoll(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-primary btn-sm">
<i class="fa fa-play" aria-hidden="true"></i> <span i18n>运行 (Roll)</span></button>
<button i18n *ngIf="currentApp.runnable() && currentApp.actions.get('custom')" [disabled]="!appsService.allReady(currentApp)" (click)="custom(currentApp)" type="button" class="btn btn-secondary btn-sm">设置</button>
<!--<div id="network" *ngIf="currentApp.network && currentApp.network.protocol == 'maotama'">-->
<!--<div class="input-group input-group-sm">-->
<!--<input *ngIf="appsService.connections.get(currentApp)" [value]="appsService.connections.get(currentApp).address || 'Loading...'" readonly type="text" class="form-control" title="address">-->
<!--<div class="input-group-btn" style="flex-direction: row">-->
<!--<button i18n *ngIf="!appsService.connections.get(currentApp)" [disabled]="!appsService.allReady(currentApp)" (click)="appsService.network(currentApp, currentApp.network.servers[0])" type="button" class="btn btn-secondary btn-sm">联机</button>-->
<!--<button i18n *ngIf="appsService.connections.get(currentApp)" (click)="copy(appsService.connections.get(currentApp).address)" [disabled]="!appsService.connections.get(currentApp).address" type="button" class="btn btn-secondary btn-sm">复制</button>-->
<!--<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>-->
<!--<div class="dropdown-menu" [class.dropdown-menu-right]="appsService.connections.get(currentApp)">-->
<!--<h6 i18n class="dropdown-header">选择服务器</h6>-->
<!--<a *ngFor="let server of currentApp.network.servers" (click)="appsService.network(currentApp, server)" class="dropdown-item" href="#">{{server.id}}</a>-->
<!--<div *ngIf="appsService.connections.get(currentApp)" class="dropdown-divider"></div>-->
<!--<a i18n *ngIf="appsService.connections.get(currentApp)" (click)="appsService.connections.get(currentApp).connection.close()" class="dropdown-item" href="#">取消</a>-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
<!--</div>-->
</div>
<!--<button (click)="log(appsService)">test</button>-->
<network *ngIf="currentApp && !currentApp.isYGOPro && currentApp.network && currentApp.network.protocol == 'maotama'" [currentApp]="currentApp"></network>
<ygopro *ngIf="currentApp.isReady() && currentApp.isYGOPro" [app]="currentApp" [currentApp]="currentApp" (points)="onPoints($event)"></ygopro>
</div>
</div>
<div id="arena" class="panel panel-default" *ngIf="currentApp.isYGOPro && points ">
<h2 i18n>排位成绩</h2>
<table class="table table-sm">
<tbody>
<tr>
<th i18n>D.P</th>
<td>{{points.pt}}</td>
<th i18n>经验</th>
<td>{{points.exp}}</td>
</tr>
<tr>
<th i18n>竞技胜率</th>
<td>{{points.athletic_wl_ratio}}%</td>
<th i18n>竞技排名</th>
<td>{{points.arena_rank}}</td>
</tr>
<tr>
<th i18n>竞技胜场</th>
<td>{{points.athletic_win}}</td>
<th i18n>竞技总场</th>
<td>{{points.athletic_all}}</td>
</tr>
<tr>
<th i18n>竞技负场</th>
<td>{{points.athletic_lose}}</td>
<th i18n>娱乐排名</th>
<td>{{points.exp_rank}}</td>
</tr>
<tr>
<th i18n>竞技平局</th>
<td>{{points.athletic_draw}}</td>
<th i18n>娱乐总场</th>
<td>{{points.entertain_all}}</td>
</tr>
</tbody>
</table>
<a i18n href="https://mycard.moe/ygopro/arena/" target="_blank" class="moreinfo">更多资料</a>
</div>
<div *ngIf="currentApp.description" class="d-flex">
<div class="panel" id="game_info">
<p [innerHTML]="currentApp.description"></p>
<div id="tags" *ngIf="currentApp.tags">
<div *ngFor="let tag of currentApp.tags" class="btn btn-sm btn-info tag">{{tags[tag] || tag}}</div>
</div>
</div>
<div class="panel" id="game_info_2">
<dl>
<dt i18n>开发</dt>
<dd>
<div *ngFor="let developer of currentApp.developers">
<a *ngIf="developer.url" target="_blank" [href]="developer.url">{{developer.name}}</a>
<span *ngIf="!developer.url">{{developer.name}}</span>
</div>
</dd>
<dt i18n>发行日期</dt>
<dd>{{currentApp.released_at | date:'mediumDate'}}</dd>
<dt i18n *ngIf="currentApp.updated_at">更新日期</dt>
<dd *ngIf="currentApp.updated_at">{{currentApp.updated_at | date:'mediumDate'}}</dd>
</dl>
</div>
</div>
<div class="panel panel-default" *ngIf="news && news.length">
<h2 i18n>新闻</h2>
<div id="news" *ngFor="let item of news">
<h3><a class="title" [href]="item.url" target="_blank">{{item.title}}</a></h3>
<span>{{item.updated_at | date:'shortDate'}}</span>
<p>{{item.text}}</p>
<a i18n *ngIf="item.url" [href]="item.url" target="_blank">了解更多</a>
</div>
<!--<a href="https://mycard.moe/ygopro/arena/" target="_blank" class="moreinfo">查看所有新闻</a>-->
</div>
<div *ngIf="currentApp.isReady()">
<div class="panel" *ngIf="mods && mods.length">
<h2 i18n>扩展/运行库</h2>
<table class="table table-striped expansions">
<thead>
<tr>
<th>#</th>
<th i18n>名称</th>
<th i18n>操作</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let mod of mods; let i = index">
<th scope="row">{{i + 1}}</th>
<td title="{{mod.description}}">{{mod.name}}
<span *ngIf="mod.description" class="fa fa-info-circle" aria-hidden="true"></span></td>
<td *ngIf="mod.isReady()">
<button i18n type="button" [disabled]="mod.isInstalled()&&!appsService.allReady(mod)" (click)="uninstall(mod)" class="btn btn-danger btn-sm">卸载</button>
</td>
<td *ngIf="!mod.isInstalled()">
<button i18n (click)="installMod(mod)" [disabled]="mod.isInstalled()&&!appsService.allReady(mod)" type="button" *ngIf="!mod.isInstalled()" class="btn btn-primary btn-sm">安装</button>
</td>
<td *ngIf="mod.isInstalled()&&!mod.isReady()">
<progress class="progress progress-striped progress-animated" value="{{mod.status.total ? mod.status.progress : 1}}" max="{{mod.status.total}}"></progress>
<!--<div i18n *ngIf="mod.isWaiting()">等待安装...</div>-->
</td>
</tr>
</tbody>
</table>
</div>
<div class="panel panel-default" id="local">
<h2 i18n>本地文件</h2>
<div>
<button i18n (click)="appsService.browse(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-secondary btn-sm">浏览本地文件</button>
<button i18n type="button" (click)="verifyFiles(currentApp)" [disabled]="!appsService.allReady(currentApp)" class="btn btn-secondary btn-sm">校验完整性</button>
<button i18n (click)="uninstall(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-secondary btn-sm">卸载</button>
</div>
</div>
</div>
<!--<div class="panel panel-default">--><!--<h2 i18n>广告</h2>--><!--<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>--><!--&lt;!&ndash; mycard &ndash;&gt;--><!--<ins class="adsbygoogle"--><!--style="display:block"--><!--data-ad-client="ca-pub-1173264056684633"--><!--data-ad-slot="3903147661"--><!--data-ad-format="auto"></ins>--><!--<script>--><!--(adsbygoogle = window.adsbygoogle || []).push({});--><!--</script>--><!--</div>-->
<!--安装modal-->
<div class="modal fade" id="install-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<form id="install-form" class="modal-content" (ngSubmit)="install(currentApp,installOption,referencesInstall)" #theForm="ngForm">
<div class="modal-header">
<h5 i18n class="modal-title" id="myModalLabel">安装 {{currentApp.name}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p i18n>即将开始安装 {{currentApp.name}}</p>
<h4 i18n>安装位置</h4>
<div class="form-group">
<select class="form-control" name="installPath" (change)="selectLibrary()" [(ngModel)]="installOption.installLibrary" title="path">
<option *ngFor="let library of libraries" value="{{library}}"> {{library}}</option>
<option *ngFor="let library of availableLibraries" value="create_{{library}}">在 {{library}}\ 盘新建 MyCard 库</option>
</select></div>
<h4 i18n>快捷方式</h4>
<div class="checkbox">
<input id="create_application_shortcut" type="checkbox" name="application" [(ngModel)]="installOption.createShortcut">
<label i18n *ngIf="platform == 'linux'" for="create_application_shortcut">创建启动菜单快捷方式</label>
<label i18n *ngIf="platform == 'darwin'" for="create_application_shortcut">创建 LaunchPad 快捷方式</label>
<label i18n *ngIf="platform == 'win32'" for="create_application_shortcut">创建开始菜单快捷方式</label>
</div>
<div class="checkbox">
<input id="create_desktop_shortcut" type="checkbox" name="desktop" [(ngModel)]="installOption.createDesktopShortcut">
<label i18n for="create_desktop_shortcut">创建桌面快捷方式</label>
</div>
<h4 i18n *ngIf="references.length>0">扩展内容</h4>
<div *ngFor="let reference of references"><label>
<input type="checkbox" [(ngModel)]="referencesInstall[reference.id]" name="references"> {{reference.name}}
</label></div>
<div *ngIf="currentApp.findDependencies().length">
<span i18n>依赖:</span>
<span class="dependency" *ngFor="let dependency of currentApp.findDependencies()">{{dependency.name}}</span>
</div>
</div>
<div class="modal-footer">
<button i18n type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button i18n type="submit" [disabled]="!theForm.form.valid" class="btn btn-primary">安装</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="import-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<form id="import-form" class="modal-content" (ngSubmit)="importGame(file.files![0]!.path,currentApp,installOption,referencesInstall)" #theForm="ngForm" ngNativeValidate>
<div class="modal-header">
<h5 i18n class="modal-title">导入 {{currentApp.name}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p i18n>选择主程序 {{currentApp.actions.get('main')!.execute}}</p>
<input #file name="file" type="file" id="file" class="form-control" required>
<h4 i18n>导入到</h4>
<div class="form-group">
<select class="form-control" name="installPath" (change)="selectLibrary()" [(ngModel)]="installOption.installLibrary" title="path">
<option *ngFor="let library of libraries" value="{{library}}"> {{library}}</option>
<option *ngFor="let library of availableLibraries" value="create_{{library}}">在 {{library}}\ 盘新建 MyCard 库</option>
</select>
</div>
<!--<h4 i18n>快捷方式</h4>-->
<!--<div class="checkbox">-->
<!--<input id="create_application_shortcut" type="checkbox" name="application" [(ngModel)]="installOption.createShortcut">-->
<!--<label i18n *ngIf="platform == 'darwin'" for="create_application_shortcut">创建 LaunchPad 快捷方式</label>-->
<!--<label i18n *ngIf="platform == 'win32'" for="create_application_shortcut">创建开始菜单快捷方式</label>-->
<!--</div>-->
<!--<div class="checkbox">-->
<!--<input id="create_desktop_shortcut" type="checkbox" name="desktop" [(ngModel)]="installOption.createDesktopShortcut">-->
<!--<label i18n for="create_desktop_shortcut">创建桌面快捷方式</label>-->
<!--</div>-->
<h4 i18n *ngIf="references.length>0">扩展内容</h4>
<div *ngFor="let reference of references"><label>
<input type="checkbox" [(ngModel)]="referencesInstall[reference.id]" name="references"> {{reference.name}}
</label></div>
<div *ngIf="currentApp.findDependencies().length">
<span i18n>依赖:</span>
<span class="dependency" *ngFor="let dependency of currentApp.findDependencies()">{{dependency.name}}</span>
</div>
</div>
<div class="modal-footer">
<button i18n type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button i18n type="submit" class="btn btn-primary">导入</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="purchase-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" *ngIf="!currentApp.isBought()">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 i18n class="modal-title">购买 {{currentApp.name}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!--<div class="form-group">-->
<!--<label for="exampleSelect1">国家/地区</label>-->
<!--<select class="form-control" id="exampleSelect1">-->
<!--<option>中国(¥)</option>-->
<!--<option>美国($)</option>-->
<!--<option>3</option>-->
<!--<option>4</option>-->
<!--<option>5</option>-->
<!--</select>-->
<!--</div>-->
<div class="d-flex justify-content-between">
<img *ngIf="currentApp.cover" [src]="currentApp.cover" class="banner">
<span class="p-2">{{currentApp.name}}</span>
<span class="p-2 ml-auto">{{currentApp.price.cny | currency:'CNY':true}}</span>
</div>
<form id="purchase-form" class="form-inline">
<!--<fieldset class="form-group">-->
<legend>支付方式</legend>
<div class="form-check">
<input [(ngModel)]="payment" id="alipay" type="radio" class="form-check-input" name="payment" value="alipay" checked>
<label for="alipay" class="form-check-label">支付宝</label>
</div>
<div class="form-check">
<input [(ngModel)]="payment" id="wechat" type="radio" class="form-check-input" name="payment" value="wechat" checked>
<label for="wechat" class="form-check-label">微信</label>
</div>
<!--<div class="form-check">-->
<!--<input id="paypal" type="radio" class="form-check-input" name="optionsRadios" value="alipay" checked>-->
<!--<label for="paypal" class="form-check-label">PayPal</label>-->
<!--</div>-->
<!--</fieldset>-->
</form>
</div>
<div class="modal-footer">
<button i18n class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<!-- <button i18n [disabled]="creating_order" class="btn btn-primary" (click)="purchase()">购买</button>-->
</div>
</div>
</div>
</div>
<div class="modal fade" id="purchase-modal-alipay" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" data-backdrop="static" *ngIf="!currentApp.isBought()">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 i18n class="modal-title">购买 {{currentApp.name}}</h5>
<!--<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">-->
<!--<span>&times;</span>-->
<!--</button>-->
</div>
<div class="modal-body">
订单已经创建,请在新窗口中进行支付,支付成功后页面会自动跳转
若支付成功后没有自动跳转 请联系 thdod@mycard.moe
若支付失败,请返回并选择其他支付方式
</div>
<div class="modal-footer">
<button i18n type="button" class="btn btn-secondary" data-bs-dismiss="modal">返回</button>
</div>
</div>
</div>
</div>
import { ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { AppsService } from '../apps.service';
import { InstallOption } from '../shared/install-option';
import { SettingsService } from '../settings.service';
import { App } from '../shared/app';
import { DownloadService } from '../download.service';
import { clipboard } from 'electron';
import remote from '@electron/remote';
import path from 'path';
import fs from 'fs';
// import { Points } from '../ygopro/ygopro.component';
import { LoginService } from '../login/login.service';
declare const Notification: any;
// declare interface Window {
// adsbygoogle: any[];
// }
//
// declare var adsbygoogle: any[];
@Component({
selector: 'app-detail',
templateUrl: 'app-detail.component.html',
styleUrls: ['app-detail.component.css']
})
export class AppDetailComponent implements OnInit, OnChanges {
@Input()
currentApp: App;
platform = process.platform;
installOption: InstallOption;
availableLibraries: string[] = [];
references: App[];
referencesInstall: { [id: string]: boolean };
// import_path: string;
background: string;
points: any;
tags: {};
payment = 'alipay';
creating_order = false;
constructor(public appsService: AppsService, private settingsService: SettingsService, private downloadService: DownloadService, private ref: ChangeDetectorRef, private el: ElementRef, private loginService: LoginService) {
this.tags = this.settingsService.getLocale().startsWith('zh') ? {
'recommend': '推荐',
'mysterious': '迷之物体',
'touhou': '东方 Project',
'touhou_pc98': '东方旧作',
'language': '语言包',
'ygopro': 'YGOPro'
} : {
'recommend': 'Recommended',
'mysterious': 'Something',
'touhou': 'Touhou Project',
'touhou_pc98': 'Touhou old series',
'language': 'Language Pack',
'ygopro': 'YGOPro'
};
}
async ngOnChanges(changes: SimpleChanges) {
// if (this.currentApp.isBought()) {
// $('#purchase-modal-alipay').modal('hide');
// }
if (changes['currentApp']) {
if (this.currentApp.background) {
this.el.nativeElement.style.background = `url("${this.currentApp.background}") rgba(255,255,255,.8)`;
} else {
this.el.nativeElement.style.background = 'white';
}
this.updateInstallOption(this.currentApp);
// let top = await this.http.get('https://ygobbs.com/top.json').map(response => response.json()).toPromise();
// console.log(top.topic_list.topics);
// (adsbygoogle = window['adsbygoogle'] || []).push({});
}
}
async ngOnInit(): Promise<void> {
let volume = 'A';
for (let i = 0; i < 26; i++) {
await new Promise((resolve, reject) => {
let currentVolume = String.fromCharCode(volume.charCodeAt(0) + i) + ':';
fs.access(currentVolume, (err) => {
if (!err) {
// 判断是否已经存在Library
if (this.libraries.every((library) => !library.startsWith(currentVolume))) {
this.availableLibraries.push(currentVolume);
}
}
resolve(null);
});
});
}
}
updateInstallOption(app: App) {
this.installOption = new InstallOption(app);
this.installOption.installLibrary = this.settingsService.getDefaultLibrary().path;
this.references = Array.from(app.references.values());
this.referencesInstall = {};
for (let reference of this.references) {
if (reference.isLanguage()) {
// 对于语言包,只有在语言包的locales比游戏本身的更加合适的时候才默认勾选
// 这里先偷个懒,中文环境勾选中文语言包,非中文环境勾选非中文语言包
this.referencesInstall[reference.id] =
reference.locales[0].startsWith('zh') === this.settingsService.getLocale().startsWith('zh');
} else {
this.referencesInstall[reference.id] = true;
}
}
}
get libraries(): string[] {
return this.settingsService.getLibraries().map((item) => item.path);
}
get news() {
return this.currentApp.news;
}
get mods(): App[] {
return this.appsService.findChildren(this.currentApp);
}
async installMod(mod: App) {
let option = new InstallOption(mod, path.dirname(mod.parent!.local!.path));
await this.install(mod, option, {});
}
async uninstall(app: App) {
if (confirm('确认删除?')) {
try {
await this.appsService.uninstall(app);
} catch (e) {
alert(e);
}
}
}
async install(targetApp: App, options: InstallOption, referencesInstall: { [id: string]: boolean }) {
// $('#install-modal').modal('hide');
try {
await this.appsService.install(targetApp, options);
for (let [id, install] of Object.entries(referencesInstall)) {
if (install) {
let reference = targetApp.references.get(id)!;
console.log('reference install ', id, targetApp, targetApp.references, reference);
await this.appsService.install(reference, options);
}
}
} catch (e) {
console.error(e);
new Notification(targetApp.name, { body: '下载失败' });
}
}
async selectLibrary() {
if (this.installOption.installLibrary.startsWith('create_')) {
let volume = this.installOption.installLibrary.slice(7);
let library = path.join(volume, 'MyCardLibrary');
try {
await this.appsService.createDirectory(library);
this.installOption.installLibrary = library;
this.settingsService.addLibrary(library, true);
} catch (e) {
this.installOption.installLibrary = this.settingsService.getDefaultLibrary().path;
alert('无法创建指定目录');
} finally {
let index = this.availableLibraries.findIndex((l) => {
return l === volume;
});
this.availableLibraries.splice(index, 1);
}
} else {
this.settingsService.setDefaultLibrary({ path: this.installOption.installLibrary, 'default': true });
}
this.installOption.installLibrary = this.settingsService.getDefaultLibrary().path;
}
selectDir() {
let dir = remote.dialog.showOpenDialog({ properties: ['openFile', 'openDirectory'] });
console.log(dir);
// this.appsService.installOption.installDir = dir[0];
return dir[0];
}
runApp(app: App, action_name = 'main') {
this.appsService.runApp(app, action_name);
}
custom(app: App) {
this.appsService.runApp(app, 'custom');
}
async runRoll(app: App) {
await this.appsService.runApp(app, 'roll_main');
await this.appsService.runApp(app, 'roll');
}
async importGame(origin: string, targetApp: App, option: InstallOption, referencesInstall: { [id: string]: boolean }) {
let dir = path.dirname(origin);
// TODO: 执行依赖和references安装
try {
await this.appsService.importApp(targetApp, dir, option);
for (let [id, install] of Object.entries(referencesInstall)) {
if (install) {
let reference = targetApp.references.get(id)!;
console.log('reference install ', id, targetApp, targetApp.references, reference);
await this.appsService.install(reference, option);
}
}
} catch (e) {
console.error(e);
new Notification(targetApp.name, { body: '导入失败' });
}
}
async verifyFiles(app: App) {
try {
await this.appsService.update(app, true);
let installedMods = this.appsService.findChildren(app).filter((child) => {
return child.parent === app && child.isInstalled() && child.isReady();
});
for (let mod of installedMods) {
await this.appsService.update(mod, true);
}
} catch (e) {
new Notification(app.name, { body: '校验失败' });
console.error(e);
}
}
copy(text: string) {
clipboard.writeText(text);
}
// async selectImport(app: App) {
// let main = app.actions.get('main');
// if (!main) {
// return;
// }
// if (!main.execute) {
// return;
// }
// let filename = main.execute.split('/')[0];
// let extname = path.extname(filename).slice(1);
//
// // let remote = require('electron').remote
// let filePaths = await new Promise<string[]>((resolve, reject) => {
// remote.dialog.showOpenDialog({
// filters: [{name: filename, extensions: [extname]}],
// properties: ['openFile']
// }, resolve);
// });
//
// if (filePaths && filePaths[0]) {
// this.import_path = filePaths[0];
// }
//
// }
onPoints(points: any) {
this.points = points;
}
// async purchase() {
// this.creating_order = true;
// let data = new URLSearchParams();
// data.set('app_id', this.currentApp.id);
// data.set('user_id', this.loginService.user.email);
// data.set('currency', 'cny');
// data.set('payment', this.payment);
// try {
// let {url} = await this.http.post('https://sapi.moecube.com:444/orders', data).map(response => response.json()).toPromise();
// open(url);
// $('#purchase-modal').modal('hide');
// $('#purchase-modal-alipay').modal('show');
// } catch (error) {
// console.log(error);
// if (error.status === 409) {
// alert('卖完了 /\\');
// } else if (error.status === 403) {
// alert('已经购买过 /\\');
// } else {
// alert('出错了 /\\');
// }
// }
// this.creating_order = false;
// }
}
......@@ -2,23 +2,33 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule, NO_ERRORS_SCHEMA } from '@angular/cor
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { MyCardComponent } from './mycard.component';
import { MyCardComponent } from './mycard/mycard.component';
// import { WebviewDirective } from './shared/webview.directive';
import { FormsModule } from '@angular/forms';
import { LoginComponent } from './login/login.component';
import { LobbyComponent } from './lobby/lobby.component';
import { AppDetailComponent } from './app-detail/app-detail.component';
import { CandyComponent } from './candy/candy.component';
import { NetworkComponent } from './network/network.component';
import { YGOProComponent } from './ygopro/ygopro.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
MyCardComponent,
// WebviewDirective,
LoginComponent,
LobbyComponent
LobbyComponent,
AppDetailComponent,
CandyComponent,
NetworkComponent,
YGOProComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule
FormsModule,
HttpClientModule
],
providers: [],
bootstrap: [MyCardComponent],
......
import { ApplicationRef, EventEmitter, Injectable, NgZone } from '@angular/core';
import child_process from 'child_process';
import { ChildProcess } from 'child_process';
import crypto from 'crypto';
import * as remote from '@electron/remote';
import * as sudo from 'child_process';
import fs from 'fs';
import glob from 'glob';
import path from 'path';
import readline from 'readline';
import { App, AppStatus } from './shared/app';
import { AppLocal } from './shared/app-local';
import { DownloadService, DownloadStatus } from './download.service';
import { InstallOption } from './shared/install-option';
import { LoginService } from './login/login.service';
import { SettingsService } from './settings.service';
import { ComparableSet } from './shared/ComparableSet';
import { AppsJson } from './shared/apps-json-type';
import os from 'os';
import Timer = NodeJS.Timer;
import { HttpClient } from '@angular/common/http';
import { map, timeout } from 'rxjs/operators';
import { Observable, Observer } from 'rxjs';
const Logger = {
info: (...message: any[]) => {
console.log('AppService [INFO]: ', ...message);
},
error: (...message: any[]) => {
console.error('AppService [ERROR]: ', ...message);
}
};
interface InstallTask {
app: App;
option: InstallOption;
}
interface InstallStatus {
status: string;
progress: number;
total: number;
lastItem: string;
}
interface Connection {
connection: WebSocket;
address: string | null;
}
declare const System: any;
@Injectable({
providedIn: 'root'
})
export class AppsService {
eventEmitter = new EventEmitter<void>();
map: Map<string, string> = new Map();
connections = new Map<App, Connection>();
maotama: Promise<ChildProcess>;
private systemBinPath(executableName: string) {
return path.join(process.env['NODE_ENV'] === 'production' ? process.resourcesPath! : '', 'bin', executableName);
}
private get tarPath() {
if (process.platform === 'linux') {
return 'tar';
} else if (process.platform === 'win32') {
return this.systemBinPath('bsdtar.exe');
} else {
return this.systemBinPath('gtar');
}
}
private getTarStream(p: child_process.ChildProcessWithoutNullStreams) {
return process.platform === 'win32' ? p.stderr : p.stdout;
}
private apps: Map<string, App>;
constructor(private http: HttpClient, private settingsService: SettingsService, private ref: ApplicationRef,
private downloadService: DownloadService, private ngZone: NgZone, private loginService: LoginService) {
}
get lastVisited(): App | undefined {
let id = localStorage.getItem('last_visited');
if (id) {
return this.apps.get(id);
}
return undefined;
}
set lastVisited(app: App | undefined) {
if (app) {
localStorage.setItem('last_visited', app.id);
}
}
async loadApps() {
let appsURL = 'https://sapi.moecube.com:444/release/update/apps.json';
let keysURL = 'https://sapi.moecube.com:444/keys';
try {
let data = await this.http.get<AppsJson.App[]>(appsURL).pipe(timeout(5000)).toPromise();
let keys_data = await this.http.get<any[]>(keysURL, {
params: {
user_id: this.loginService.user.email
}
}).toPromise();
for (let item of keys_data) {
let app = data.find((app: any) => app.id === item.app_id);
if (app) {
app.key = item.id;
}
}
localStorage.setItem('apps_json', JSON.stringify(data));
this.apps = this.loadAppsList(data);
} catch (e) {
console.error(e);
let data = localStorage.getItem('apps_json');
if (data) {
new Notification('MyCard', { body: '读取最新游戏列表失败...' });
this.apps = this.loadAppsList(JSON.parse(data!));
} else {
alert('读取游戏列表失败,可能是网络不通');
this.apps = new Map();
}
}
return this.apps;
}
async migrate() {
await this.bundle();
await this.migrate_v2_ygopro();
await this.migreate_library();
}
async bundle() {
try {
// const bundle = require(path.join(remote.app.getPath('appData'), 'mycard', 'bundle.json'));
// 示例:
// [
// {
// "app": "th105",
// "createShortcut": false,
// "createDesktopShortcut": false,
// "install": true,
// "installDir": "D:\\MyCardLibrary\\apps\\th105",
// "installLibrary": "D:\\MyCardLibrary"
// },
// {
// "app": "th105-lang-zh-CN",
// "createShortcut": false,
// "createDesktopShortcut": false,
// "install": true,
// "installDir": "D:\\MyCardLibrary\\apps\\th105",
// "installLibrary": "D:\\MyCardLibrary"
// },
// {
// "app": "th123",
// "createShortcut": false,
// "createDesktopShortcut": true,
// "install": true,
// "installDir": "D:\\MyCardLibrary\\apps\\th123",
// "installLibrary": "D:\\MyCardLibrary"
// },
// {
// "app": "th123-lang-zh-CN",
// "createShortcut": false,
// "createDesktopShortcut": false,
// "install": true,
// "installDir": "D:\\MyCardLibrary\\apps\\th123",
// "installLibrary": "D:\\MyCardLibrary"
// },
// {
// "app": "directx",
// "createShortcut": false,
// "createDesktopShortcut": false,
// "install": true,
// "installDir": "D:\\MyCardLibrary\\apps\\directx",
// "installLibrary": "D:\\MyCardLibrary"
// },
// ]
// {
// library: "D:\\MyCardLibrary",
// apps: ["th105", "th105-lang-zh-CN", "th123", "th123-lang-zh-CN", "directx"]
// }
// 文件在 D:\MyCardLibrary\cache\th105.tar.xz, D:\MyCardLibrary\cache\th105-lang-zh-CN.tar.xz ...
// TODO: 安装那些app,不需要下载。安装成功后删除 bundle.json
} catch (error) {
}
}
async migrate_v2_ygopro() {
// 导入萌卡 v2 的 YGOPRO
let app = this.apps.get('ygopro')!;
if (app.isInstalled() || localStorage.getItem('migrate_v2_ygopro')) {
return;
}
try {
const legacy_ygopro_path = System._nodeRequire(path.join(remote.app.getPath('appData'), 'mycard', 'db.json')).local.ygopro.path;
if (legacy_ygopro_path) {
// TODO: 导入YGOPRO
// 示例: "C:\\Users\\a915329096\\AppData\\Roaming\\mycard\\apps\\ygopro"
// 不带任何reference,如果同盘符已有库,安装到那个库里,否则在那个盘符建个库。
let library: string | undefined;
if (process.platform === 'win32') {
let volume = legacy_ygopro_path.split(':')[0].toUpperCase();
for (let _library of this.settingsService.getLibraries()) {
if (_library.path.split(':')[0].toUpperCase() === volume) {
library = _library.path;
}
}
if (!library) {
try {
let _library = path.join(volume + ':', 'MyCardLibrary');
await this.createDirectory(_library);
this.settingsService.addLibrary(_library, true);
library = _library;
} catch (error) {
}
}
}
if (!library) {
library = this.settingsService.getDefaultLibrary().path;
}
let option = new InstallOption(app, library, false, false);
console.log('migrate ygopro', legacy_ygopro_path, library);
await this.importApp(app, legacy_ygopro_path, option);
localStorage.setItem('migrate_v2_ygopro', 'true');
}
} catch (error) {
}
}
async migreate_library() {
let libraries = this.settingsService.getLibraries();
for (let library of libraries) {
if (library.path === path.join(remote.app.getPath('appData'), 'library')) {
library.path = path.join(remote.app.getPath('appData'), 'MyCardLibrary');
}
}
localStorage.setItem(SettingsService.SETTING_LIBRARY, JSON.stringify(libraries));
}
loadAppsList = (data: any): Map<string, App> => {
let apps = new Map<string, App>();
let locale = this.settingsService.getLocale();
let platform = process.platform;
for (let item of data) {
let app = new App(item);
let local = localStorage.getItem(app.id);
if (item.files) {
app.files = new Map(Object.entries(item.files));
} else {
app.files = new Map();
}
if (local) {
app.local = new AppLocal();
app.local.update(JSON.parse(local));
}
app.status = new AppStatus();
if (local) {
app.status.status = 'ready';
} else {
app.reset();
}
// 去除无关语言
for (let key of ['name', 'description', 'news', 'developers', 'publishers']) {
if (app[key]) {
app[key] = app[key][locale] || app[key]['zh-CN'] || Object.values(app[key])[0];
}
}
// 去除平台无关的内容
for (let key of ['actions', 'dependencies', 'references', 'version']) {
if (app[key]) {
if (app[key][platform]) {
app[key] = app[key][platform];
} else if (platform === 'linux' && app[key].darwin) {
app[key] = app[key].darwin;
} else {
app[key] = null;
}
}
}
// 时间
if (app.released_at) {
app.released_at = new Date(app.released_at);
}
if (app.updated_at) {
app.updated_at = new Date(app.updated_at);
}
if (app.news) {
for (let item of app.news) {
item.updated_at = new Date(item.updated_at);
}
}
apps.set(item.id, app);
}
// 设置App关系
for (let app of apps.values()) {
let temp = app.actions;
let map = new Map<string, any>();
for (let action of Object.keys(temp)) {
let openId = temp[action]['open'];
if (openId) {
temp[action]['open'] = apps.get(openId);
}
map.set(action, temp[action]);
}
app.actions = map;
for (let key of ['dependencies', 'references', 'parent']) {
let value = app[key];
if (value) {
if (Array.isArray(value)) {
let map = new Map<string, App>();
for (let appId of value) {
map.set(appId, apps.get(appId)!);
}
app[key] = map;
} else {
app[key] = apps.get(value);
}
}
}
// 为语言包置一个默认的名字
// 这里简易做个 i18n 的 hack
const lang = {
'en-US': {
'en-US': 'English',
'zh-CN': 'Simplified Chinese',
'zh-TW': 'Traditional Chinese',
'language_pack': 'Language Pack'
},
'zh-CN': {
'en-US': '英文',
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
'language_pack': '语言包'
}
};
if (!app.name && app.parent && app.isLanguage()) {
app.name = `${app.parent.name} ${lang[locale].language_pack} (${app.locales.map((l) => lang[locale][l]).join(', ')})`;
}
}
return apps;
};
allReady(app: App) {
return app.isReady() &&
app.findDependencies().every((dependency) => dependency.isReady()) &&
this.findChildren(app).every((child) => (child.isInstalled() && child.isReady()) || !child.isInstalled());
}
async copyFile(src: string, dst: string): Promise<any> {
return new Promise((resolve, reject) => {
let readable = fs.createReadStream(src);
readable.on('open', () => {
let writable = fs.createWriteStream(dst);
writable.on('error', reject);
writable.on('close', resolve);
readable.pipe(writable);
});
readable.on('error', reject);
});
}
async importApp(app: App, appPath: string, option: InstallOption) {
if (!app.isInstalled()) {
app.status.status = 'updating';
let checksumFiles = await this.getChecksumFile(app);
for (let [pattern, fileOption] of app.files) {
await new Promise((resolve, reject) => {
new glob.Glob(pattern, { cwd: appPath }, (err, files) => {
for (let file of files) {
// 避免被当做文件夹
if (fileOption.sync) {
checksumFiles.set(file, 'DO_NOT_CARE_HASH');
}
}
resolve(null);
});
});
}
await this.createDirectory(option.installDir);
let sortedFiles = Array.from(checksumFiles.entries()).sort((a: string[], b: string[]): number => {
if (a[0] > b[0]) {
return 1;
} else if (a[0] < b[0]) {
return -1;
} else {
return 0;
}
});
app.status.total = sortedFiles.length;
// 刷新进度
let interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => {
this.ngZone.runOutsideAngular(async () => {
try {
for (let [file, checksum] of sortedFiles) {
let src = path.join(appPath, file);
let dst = path.join(option.installDir, file);
if (checksum === '') {
await this.createDirectory(dst);
} else {
try {
await this.copyFile(src, dst);
} catch (e) {
} finally {
app.status.progress += 1;
}
}
}
resolve(null);
} catch (e) {
reject(e);
}
});
});
clearInterval(interval);
app.local = new AppLocal();
app.local.path = option.installDir;
app.status.status = 'ready';
await this.update(app, true);
this.saveAppLocal(app);
}
}
sha256sum(file: string): Promise<string> {
return new Promise((resolve, reject) => {
let input = fs.createReadStream(file);
const hash = crypto.createHash('sha256');
hash.on('error', (error: Error) => {
reject(error);
});
input.on('error', (error: Error) => {
reject(error);
});
hash.on('readable', () => {
let data = <Buffer>hash.read();
if (data) {
resolve(data.toString('hex'));
}
});
input.pipe(hash);
});
}
async verifyFiles(app: App, checksumFiles: Map<string, string>, callback: () => void): Promise<Map<string, string>> {
let result = new Map<string, string>();
for (let [file, checksum] of checksumFiles) {
let filePath = path.join(app.local!.path, file);
// 如果文件不存在,随便生成一个checksum
await new Promise((resolve, reject) => {
fs.access(filePath, fs.constants.F_OK, async (err) => {
if (err) {
result.set(file, Math.random().toString());
} else if (checksum === '') {
result.set(file, '');
} else {
let sha256sum = await this.sha256sum(filePath);
result.set(file, sha256sum);
}
callback();
resolve(null);
});
});
}
return result;
}
async update(app: App, verify = false) {
let readyToUpdate = false;
// 已经安装的mod
let mods = this.findChildren(app).filter((mod) => {
return mod.parent === app && mod.isInstalled();
});
// 如果是不是mod,那么要所有已经安装mod都ready
// 如果是mod,那么要parent ready
if (app.parent && app.parent.isReady() && app.isReady()) {
readyToUpdate = true;
} else {
readyToUpdate = app.isReady() && mods.every((mod) => mod.isReady());
}
if (readyToUpdate && (verify || app.local!.version !== app.version)) {
app.status.status = 'updating';
try {
Logger.info('Checking updating: ', app);
let latestFiles = await this.getChecksumFile(app);
let localFiles: Map<string, string> | undefined;
if (verify) {
// 刷新进度条
let interval = setInterval(() => {
}, 500);
app.status.total = latestFiles.size;
await new Promise((resolve, reject) => {
this.ngZone.runOutsideAngular(async () => {
try {
localFiles = await this.verifyFiles(app, latestFiles, () => {
app.status.progress += 1;
});
resolve(null);
} catch (e) {
reject(e);
}
});
});
clearInterval(interval);
} else {
localFiles = app.local!.files;
}
let addedFiles: Set<string> = new Set<string>();
let changedFiles: Set<string> = new Set<string>();
let deletedFiles: Set<string> = new Set<string>();
// 遍历寻找新增加的文件
for (let [file, checksum] of latestFiles) {
if (checksum !== '' && !localFiles!.has(file)) {
addedFiles.add(file);
// changedFiles包含addedFiles,addedFiles仅供mod更新的时候使用。
changedFiles.add(file);
} else if (checksum === '' && file !== '.') {
await this.createDirectory(path.join(app.local!.path, file));
}
}
let ignoreFiles: Set<string> = new Set();
for (let [pattern, fileOption] of app.files) {
await new Promise((resolve, reject) => {
new glob.Glob(pattern, { cwd: app.local!.path }, (err, files) => {
for (let file of files) {
if (fileOption.ignore) {
ignoreFiles.add(file);
}
}
resolve(null);
});
});
}
// 遍历寻找旧版本与新版本不一样的文件和新版本比旧版少了的文件
// ignoreFiles里的文件不作处理
for (let [file, checksum] of localFiles!) {
if (latestFiles.has(file)) {
let latestChecksum = latestFiles.get(file);
if (!ignoreFiles.has(file) && latestChecksum !== checksum && latestChecksum !== '') {
changedFiles.add(file);
} else if (latestChecksum === '') {
await this.createDirectory(path.join(app.local!.path, file));
}
} else {
deletedFiles.add(file);
}
}
let backupFiles: string[] = [];
let restoreFiles: string[] = [];
if (app.parent) {
let parentFiles = app.parent.local!.files;
// 新增加的文件和parent冲突,且不是目录,就添加backup到
// 改变的文件不做备份
for (let addedFile of addedFiles) {
if (parentFiles.has(addedFile) && parentFiles.get(addedFile) !== '') {
backupFiles.push(addedFile);
}
}
// 如果要删除的文件parent里也有就恢复这个文件
for (let deletedFile of deletedFiles) {
restoreFiles.push(deletedFile);
}
let backupDir = path.join(path.dirname(app.local!.path), 'backup', app.parent.id);
await this.backupFiles(app.local!.path, backupDir, backupFiles);
await this.restoreFiles(app.local!.path, backupDir, restoreFiles);
} else {
for (let mod of mods) {
// 更新时,冲突文件在backup目录里,需要更新backup目录里的文件
// 如果changed列表与已经安装的mod有冲突,就push到backup列表里
// 然后先把当前的mod文件被分到mods_backup目录再解压更新,把文件备份到backup,最后从mods_backup里恢复mods文件
// 校验时,认为mod的文件正确,把冲突文件从changed列表里面删除掉
for (let changedFile of changedFiles) {
if (mod.local!.files.has(changedFile)) {
if (!verify) {
backupFiles.push(changedFile);
} else {
changedFiles.delete(changedFile);
}
}
}
let backupToDelete: string[] = [];
// 如果要删除的文件,mod里面存在,就删除backup目录里的文件
for (let deletedFile of deletedFiles) {
if (mod.local!.files.has(deletedFile)) {
backupToDelete.push(deletedFile);
}
}
let backupDir = path.join(path.dirname(app.local!.path), 'mods_backup', app.id);
await this.backupFiles(app.local!.path, backupDir, backupFiles);
for (let file of backupToDelete) {
await this.deleteFile(path.join(app.local!.path, file));
}
}
}
await this.doUpdate(app, changedFiles, deletedFiles);
Logger.info('Update extract finished');
// 如果不是mod,就先把自己目录里最新的冲突文件backup到backup目录
// 再把mods_backup里面的文件恢复到游戏目录
if (!app.parent) {
Logger.info('Start to restore files...');
let modsBackupDir = path.join(path.dirname(app.local!.path), 'mods_backup', app.id);
let appBackupDir = path.join(path.dirname(app.local!.path), 'backup', app.id);
await this.backupFiles(app.local!.path, appBackupDir, backupFiles);
await this.restoreFiles(app.local!.path, modsBackupDir, backupFiles);
}
app.local!.version = app.version;
app.local!.files = latestFiles;
this.saveAppLocal(app);
app.status.status = 'ready';
Logger.info('Update Finished: ', app);
} catch (e) {
Logger.error('Update Failed: ', e);
// 如果导入失败,根据是否安装重置status
if (app.local!.files) {
app.status.status = 'ready';
} else {
app.reset();
}
throw e;
}
}
}
async doUpdate(app: App, changedFiles?: Set<string>, deletedFiles?: Set<string>) {
if (changedFiles && changedFiles.size > 0) {
Logger.info('Update changed files: ', changedFiles);
let locale = this.settingsService.getLocale();
if (!['zh-CN', 'en-US', 'ja-JP'].includes(locale)) {
locale = 'en-US';
}
let updateUrl = App.updateUrl(app, process.platform, locale, os.arch());
let metalink = await this.http.post(updateUrl, changedFiles, {responseType: 'text'}).toPromise();
let downloadDir = path.join(path.dirname(app.local!.path), 'downloading');
let downloadId = await this.downloadService.addMetalink(metalink, downloadDir);
await this.downloadService.progress(downloadId, (status: DownloadStatus) => {
app.status.progress = status.completedLength;
app.status.total = status.totalLength;
app.status.progressMessage = status.downloadSpeedText;
this.ref.tick();
});
let downloadFiles = await this.downloadService.getFiles(downloadId);
app.status.total = 0;
// 刷新进度条
let interval = setInterval(() => {
}, 500);
for (let downloadFile of downloadFiles) {
await new Promise((resolve, reject) => {
this.extract(downloadFile, app.local!.path).subscribe((file) => {
app.status.progressMessage = file;
}, (error) => {
reject(error);
}, () => {
resolve(null);
});
});
}
clearInterval(interval);
}
if (deletedFiles && deletedFiles.size > 0) {
Logger.info('Found files deleted: ', deletedFiles);
for (let deletedFile of deletedFiles) {
await this.deleteFile(path.join(app.local!.path, deletedFile));
}
}
}
async install(app: App, option: InstallOption) {
const tryToInstall = async (task: InstallTask): Promise<void> => {
if (!task.app.readyForInstall()) {
await new Promise((resolve, reject) => {
this.eventEmitter.subscribe(() => {
if (task.app.readyForInstall()) {
resolve(null);
} else if (task.app.findDependencies().find((dependency: App) => !dependency.isInstalled())) {
reject('Dependencies failed');
}
});
});
}
await this.doInstall(task);
};
const addDownloadTask = async (_app: App, dir: string): Promise<{ app: App, files: string[] }> => {
let locale = this.settingsService.getLocale();
if (!['zh-CN', 'en-US', 'ja-JP'].includes(locale)) {
locale = 'en-US';
}
let metalinkUrl = App.downloadUrl(_app, process.platform, locale, os.arch());
_app.status.status = 'downloading';
let metalink = await this.http.get(metalinkUrl,{responseType: 'text'}).toPromise();
let downloadId = await this.downloadService.addMetalink(metalink, dir);
try {
await this.downloadService.progress(downloadId, (status: DownloadStatus) => {
_app.status.progress = status.completedLength;
_app.status.total = status.totalLength;
_app.status.progressMessage = status.downloadSpeedText;
this.ref.tick();
});
} catch (e) {
throw e;
}
let files = await this.downloadService.getFiles(downloadId);
_app.status.status = 'waiting';
return { app: _app, files: files };
};
if (!app.isInstalled()) {
let apps: App[] = [];
let dependencies = app.findDependencies().filter((dependency) => {
return !dependency.isInstalled();
});
apps.push(...dependencies, app);
try {
let downloadPath = path.join(option.installLibrary, 'downloading');
let tasks: Promise<any>[] = [];
Logger.info('Start to Download', apps);
for (let a of apps) {
tasks.push(addDownloadTask(a, downloadPath));
}
let downloadResults = await Promise.all(tasks);
Logger.info('Download Complete', downloadResults);
let installTasks: Promise<void>[] = [];
for (let result of downloadResults) {
let o = new InstallOption(result.app, option.installLibrary);
o.downloadFiles = result.files;
let task = tryToInstall({ app: result.app, option: o });
installTasks.push(task);
}
await Promise.all(installTasks);
} catch (e) {
for (let a of apps) {
if (!a.isReady()) {
a.reset();
}
}
throw e;
}
}
}
findChildren(app: App): App[] {
let children: App[] = [];
for (let child of this.apps.values()) {
if (child.parent === app || child.dependencies && child.dependencies.has(app.id)) {
children.push(child);
}
}
return children;
}
async runApp(app: App, action_name = 'main') {
let children = this.findChildren(app);
const handle = await app.spawnApp(children, action_name);
handle.stdout.on('data', (data) => {
console.log(`${app.id} ${action_name} stdout: ${data}`);
});
handle.stderr.on('data', (data) => {
console.log(`${app.id} ${action_name} stderr: ${data}`);
});
handle.on('close', (code) => {
console.log(`${app.id} ${action_name}: child process exited with code ${code}`);
if (action_name !== 'roll') {
remote.getCurrentWindow().restore();
}
});
if (action_name !== 'roll') {
remote.getCurrentWindow().minimize();
}
}
browse(app: App) {
if (app.local) {
remote.shell.showItemInFolder(app.local.path + '/.');
}
}
async network(app: App, server: any) {
if (!this.maotama) {
this.maotama = new Promise((resolve, reject) => {
let child = (process.platform === 'linux' ? child_process : sudo).fork('maotama', [], { stdio: ['inherit', 'inherit', 'inherit', 'ipc'] }); // it's very shit of Linux electron-sudo
child.once('message', () => resolve(child));
child.once('error', reject);
child.once('exit', reject);
});
}
let child: ChildProcess;
try {
child = await this.maotama;
} catch (error) {
alert(`出错了 ${error}`);
return;
}
let connection = this.connections.get(app);
if (connection) {
connection.connection.close();
}
connection = { connection: new WebSocket(server.url), address: null };
let id: Timer | null;
this.connections.set(app, connection);
connection.connection.onmessage = (event) => {
console.log(event.data);
let [action, args] = event.data.split(' ', 2);
let [address, port] = args.split(':');
switch (action) {
case 'LISTEN':
connection!.address = args;
this.ref.tick();
break;
case 'CONNECT':
id = setInterval(() => {
child.send({
action: 'connect',
arguments: [app.network.port, port, address]
});
}, 200);
break;
case 'CONNECTED':
if (id) {
clearInterval(id);
id = null;
}
break;
}
};
connection.connection.onclose = (event: CloseEvent) => {
if (id) {
clearInterval(id);
}
// 如果还是在界面上显示的那个连接
if (this.connections.get(app) === connection) {
this.connections.delete(app);
if (event.code !== 1000 && !connection!.address) {
alert(`出错了 ${event.code}`);
}
}
// 如果还没建立好就出错了,就弹窗提示这个错误
this.ref.tick();
};
}
// tarPath: string;
// installingId: string = '';
// installQueue: Map<string,InstallTask> = new Map();
// 调用前提:应用的依赖均已 Ready,应用处于下载完待安装的状态(waiting)。
// TODO: 要把Task系统去掉吗
async doInstall(task: InstallTask) {
let app = task.app;
if (!app.isWaiting()) {
console.error('doUninstall', '应用不处于等待安装状态', app);
throw('应用不处于等待安装状态');
}
if (!app.readyForInstall()) {
console.error('doInstall', '应用依赖不齐备', app);
throw('应用依赖不齐备');
}
try {
let option = task.option;
let installDir = option.installDir;
let checksumFile = await this.getChecksumFile(app);
let allFiles = new Set(checksumFile.keys());
app.status.status = 'installing';
app.status.total = allFiles.size;
app.status.progress = 0;
let interval = setInterval(() => {
}, 500);
if (app.parent) {
// mod需要安装到parent路径
installDir = app.parent.local!.path;
let parentFiles = new ComparableSet(Array.from(app.parent.local!.files.keys()));
let appFiles = new ComparableSet(Array.from(checksumFile.keys()));
let conflictFiles = appFiles.intersection(parentFiles);
app.status.total += conflictFiles.size;
if (conflictFiles.size > 0) {
let backupPath = path.join(option.installLibrary, 'backup', app.parent.id);
// 文件夹不需要备份,删除
for (let conflictFile of conflictFiles) {
if (checksumFile.get(conflictFile) === '') {
conflictFiles.delete(conflictFile);
}
}
await new Promise((resolve, reject) => {
this.ngZone.runOutsideAngular(async () => {
try {
await this.backupFiles(app.parent!.local!.path, backupPath, conflictFiles, (n) => {
app.status.progress += 1;
});
resolve(null);
} catch (e) {
reject(e);
}
});
});
}
}
// let timeNow = new Date().getTime();
for (let file of option.downloadFiles) {
await this.createDirectory(installDir);
await new Promise((resolve, reject) => {
this.extract(file, installDir).subscribe(
(lastItem: string) => {
app.status.progress += 1;
app.status.progressMessage = lastItem;
},
(error) => {
reject(error);
},
() => {
resolve(null);
});
});
}
clearInterval(interval);
await this.postInstall(app, installDir);
console.log('post install success');
let local = new AppLocal();
local.path = installDir;
local.files = checksumFile;
local.version = app.version;
app.local = local;
this.saveAppLocal(app);
app.status.status = 'ready';
} catch (e) {
console.log('exception in doInstall', e);
throw e;
} finally {
this.eventEmitter.emit();
}
}
// 移除mkdirp函数,在这里自己实现
// 那个路径不存在且建立目录失败、或那个路径已经存在且不是目录,reject
// 那个路径已经存在且是目录,返回false,那个路径不存在且成功建立目录,返回true
// TODO: 没测试
async createDirectory(dir: string): Promise<boolean> {
let stats: fs.Stats;
try {
stats = await new Promise<fs.Stats>((resolve, reject) => {
fs.stat(dir, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
} catch (error) { // 路径不存在,先尝试递归上级目录,再创建自己
await this.createDirectory(path.dirname(dir));
return new Promise<boolean>((resolve, reject) => {
fs.mkdir(dir, (error) => {
if (error) {
reject(error);
} else {
resolve(true);
}
});
});
}
if (stats.isDirectory()) { // 路径存在并且已经是目录,成功返回
return false;
} else { // 路径存在并且不是目录,失败。
throw `#{dir} exists and is not a directory`;
}
}
extract(file: string, dir: string): Observable<string> {
return Observable.create((observer: Observer<string>) => {
const tarArgs = ['-xvf', file, '-C', dir];
if (process.platform === 'darwin') {
tarArgs.unshift(`--use-compress-program=${this.systemBinPath('zstd')}`);
}
Logger.info('Start to extract... Command Line: ' + this.tarPath, tarArgs.join(' '));
let tarProcess = child_process.spawn(this.tarPath, tarArgs);
let rl = readline.createInterface({
input: this.getTarStream(tarProcess)
});
rl.on('line', (input: string) => {
const line = input.split(' ', 2).pop()!;
observer.next(line);
});
tarProcess.on('error', (e) => {
console.log(e);
new Notification('MyCard', { body: '解压失败' + e.message });
observer.error(e.message);
});
tarProcess.on('exit', (code) => {
if (code === 0) {
observer.complete();
} else {
observer.error(code);
}
});
return () => {
};
});
}
// TODO: 与runApp合并,通用处理所有Action。
// shell: true的问题是DX特化,可以用写进app.json的方式
async postInstall(app: App, appPath: string) {
let action = app.actions.get('install');
if (action) {
let env = Object.assign({}, action.env);
let command: string[] = [];
command.push(action.execute);
command.push(...action.args);
let open = action.open;
if (open) {
let openAction: any = open.actions.get('main');
env = Object.assign(env, openAction.env);
command.unshift(...openAction.args);
command.unshift(openAction.execute);
}
return new Promise((resolve, reject) => {
// @ts-ignore
let child = child_process.spawn(command.shift()!, command, {
cwd: appPath,
env: env,
stdio: 'inherit',
shell: true
});
child.on('error', (error) => {
reject(error);
});
child.on('exit', (code) => {
if (code === 0) {
resolve(code);
} else {
reject(code);
}
});
});
}
}
saveAppLocal(app: App) {
if (app.local) {
localStorage.setItem(app.id, JSON.stringify(app.local));
}
}
async backupFiles(dir: string, backupDir: string, files: Iterable<string>, callback?: (progress: number) => void) {
let n = 0;
for (let file of files) {
await new Promise(async (resolve, reject) => {
let srcPath = path.join(dir, file);
let backupPath = path.join(backupDir, file);
await this.createDirectory(path.dirname(backupPath));
fs.unlink(backupPath, (err) => {
fs.rename(srcPath, backupPath, resolve);
});
if (callback) {
callback(n);
}
n += 1;
});
}
}
async restoreFiles(dir: string, backupDir: string, files: Iterable<string>, callback?: (progress: number) => {}) {
let n = 0;
for (let file of files) {
await new Promise((resolve, reject) => {
let backupPath = path.join(backupDir, file);
let srcPath = path.join(dir, file);
fs.unlink(srcPath, (err) => {
fs.rename(backupPath, srcPath, resolve);
});
n += 1;
if (callback) {
callback(n);
}
});
}
}
async getChecksumFile(app: App): Promise<Map<string, string>> {
let locale = this.settingsService.getLocale();
if (!['zh-CN', 'en-US', 'ja-JP'].includes(locale)) {
locale = 'en-US';
}
let checksumUrl = App.checksumUrl(app, process.platform, locale, os.arch());
return this.http.get(checksumUrl,{responseType: 'text'}).pipe(map((response) => {
let map = new Map<string, string>();
for (let line of response.split('\n')) {
if (line !== '') {
let [checksum, filename] = line.split(' ', 2);
// checksum文件里没有文件夹,这里添加上
map.set(path.dirname(filename), '');
map.set(filename, checksum);
}
}
return map;
})).toPromise();
}
deleteFile(file: string): Promise<string> {
return new Promise((resolve, reject) => {
fs.lstat(file, (err, stats) => {
if (err) {
return resolve(file);
}
if (stats.isDirectory()) {
fs.rmdir(file, (error) => {
resolve(file);
});
} else {
fs.unlink(file, (error) => {
resolve(file);
});
}
});
});
}
async uninstall(app: App) {
let children = this.findChildren(app);
let hasInstalledChild = children.find((child) => {
return child.isInstalled() && child.parent !== app;
});
if (hasInstalledChild) {
throw '无法卸载,还有依赖此程序的游戏。';
} else if (app.isReady()) {
for (let child of children) {
if (child.parent === app && child.isReady()) {
await this.doUninstall(child);
}
}
return await this.doUninstall(app);
}
}
// 调用前提:应用是 Ready, 不存在依赖这个应用的其他应用
async doUninstall(app: App) {
if (!app.isReady()) {
console.error('doUninstall', '应用不是 Ready 状态', app);
throw '应用不是 Ready 状态';
}
if (this.findChildren(app).find((child) => child.isInstalled())) {
console.error('doUninstall', '无法卸载,还有依赖此程序的游戏。', app);
throw '无法卸载,还有依赖此程序的游戏。';
}
app.status.status = 'uninstalling';
let appDir = app.local!.path;
let files = Array.from(app.local!.files.keys()).sort().reverse();
app.status.total = files.length;
// 500毫秒手动刷新,避免文件过多产生的性能问题
let interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => {
this.ngZone.runOutsideAngular(async () => {
try {
for (let file of files) {
app.status.progress += 1;
await this.deleteFile(path.join(appDir, file));
}
if (app.parent) {
// TODO: 建立Library模型,把拼路径的事情交给Library
let backupDir = path.join(path.dirname(appDir), 'backup', app.parent.id);
let fileSet = new ComparableSet(files);
let parentSet = new ComparableSet(Array.from(app.parent.local!.files.keys()));
let difference = parentSet.intersection(fileSet);
if (difference) {
await this.restoreFiles(appDir, backupDir, Array.from(difference));
}
}
resolve(null);
} catch (e) {
reject(e);
}
});
});
clearInterval(interval);
app.reset();
}
showResult(url: string, data: any, width = 320, height = 180) {
const data_str = JSON.stringify(data);
const BrowserWindow = remote.BrowserWindow;
width += 10;
height += 10;
let y = screen.availHeight - height;
let x = screen.availWidth - width;
let littleWindow = new BrowserWindow({
width: width,
height: height,
x: x,
y: y,
frame: process.platform === 'darwin',
titleBarStyle: process.platform === 'darwin' ? 'hidden' : undefined,
parent: remote.getCurrentWindow()
});
littleWindow.on('closed', function() {
littleWindow = null!;
});
let urlt = new URL(url, window.location.toString());
urlt.searchParams.set('data', data_str);
littleWindow.loadURL(urlt.toString());
remote.ipcMain.on('massage', () => {
alert('from littleWindow');
});
}
}
<!--<div id="candy" data-MinOrMax="default">-->
<!--</div>-->
<!--<div style="position:absolute; top:5px; right:10px;">-->
<!-- <i id="minimize" class="fa fa-minus hover-color" (click)="minimize()" data-size="" i18n-title title="最小化"></i>-->
<!-- <i id="unminimize" class="fa fa-minus hover-color" (click)="restore()" data-size="" i18n-title title="取消最小化" hidden></i>-->
<!-- <i id="restore" class="fa fa fa-chevron-down hover-color" (click)="restore()" data-size="" i18n-title title="还原" hidden></i>-->
<!-- <i id="maximize" class="fa fa fa-chevron-up hover-color" (click)="maximize()" i18n-title title="最大化"></i>-->
<!--</div>-->
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-candy',
templateUrl: './candy.component.html',
styleUrls: ['./candy.component.css']
})
export class CandyComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
/**
* Created by weijian on 2016/10/26.
*/
import {EventEmitter, Injectable, NgZone} from '@angular/core';
import { HttpClient } from '@angular/common/http';
// import {error} from 'util';
// import Timer = NodeJS.Timer;
// const Logger = {
// 'error': (message: string) => {
// console.error('DownloadService: ', message);
// }
// };
import Aria2 from 'aria2';
const MAX_LIST_NUM = 1000;
const ARIA2_INTERVAL = 500;
export class DownloadStatus {
completedLength: number;
downloadSpeed: number;
get downloadSpeedText (): string {
if (!isNaN(this.downloadSpeed) && this.downloadSpeed !== 0) {
const speedUnit = ['Byte/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s'];
let currentUnit = Math.floor(Math.log(this.downloadSpeed) / Math.log(1024));
return (this.downloadSpeed / 1024 ** currentUnit).toFixed(1) + ' ' + speedUnit[currentUnit];
}
return '';
};
gid: string;
status: string;
totalLength: number;
totalLengthText: string;
errorCode: string;
errorMessage: string;
combine (...others: DownloadStatus[]): DownloadStatus {
const priority = {
undefined: -1,
'': -1,
'active': 0,
'complete': 0,
'paused': 1,
'waiting': 1,
'removed': 2,
'error': 3
};
let status = Object.assign(new DownloadStatus(), this);
for (let o of others) {
if (priority[o.status] > priority[status.status]) {
status.status = o.status;
if (status.status === 'error') {
status.errorCode = o.errorCode;
status.errorMessage = o.errorMessage;
}
status.downloadSpeed += o.downloadSpeed;
status.totalLength += o.totalLength;
status.completedLength += o.completedLength;
}
}
return status;
}
// 0相等. 1不想等
compareTo (other: DownloadStatus): number {
if (this.status !== other.status ||
this.downloadSpeed !== other.downloadSpeed ||
this.completedLength !== other.completedLength ||
this.totalLength !== other.totalLength) {
return 1;
} else {
return 0;
}
}
constructor (item ?: any) {
if (item) {
this.completedLength = parseInt(item.completedLength) || 0;
this.downloadSpeed = parseInt(item.downloadSpeed) || 0;
this.totalLength = parseInt(item.totalLength) || 0;
this.gid = item.gid;
this.status = item.status;
this.errorCode = item.errorCode;
this.errorMessage = item.errorMessage;
} else {
this.completedLength = 0;
this.downloadSpeed = 0;
this.totalLength = 0;
}
}
}
@Injectable({
providedIn: 'root'
})
export class DownloadService {
// 强制指定IPv4,接到过一个反馈无法监听v6的。默认的host值是localhost,会连v6。
aria2 = new Aria2({host: '127.0.0.1'});
open = this.aria2.open();
updateEmitter = new EventEmitter<void>();
downloadList: Map<string, DownloadStatus> = new Map();
taskMap: Map<string, string[]> = new Map();
async refreshDownloadList () {
let activeList = await this.aria2.tellActive();
let waitList = await this.aria2.tellWaiting(0, MAX_LIST_NUM);
let stoppedList = await this.aria2.tellStopped(0, MAX_LIST_NUM);
this.downloadList.clear();
for (let item of activeList) {
this.downloadList.set(item.gid, new DownloadStatus(item));
}
for (let item of waitList) {
this.downloadList.set(item.gid, new DownloadStatus(item));
}
for (let item of stoppedList) {
this.downloadList.set(item.gid, new DownloadStatus(item));
}
this.updateEmitter.emit();
}
constructor (private ngZone: NgZone) {
ngZone.runOutsideAngular(async () => {
await this.open;
setInterval(async () => {
await this.refreshDownloadList();
}, ARIA2_INTERVAL);
});
}
private createId (): string {
function s4 () {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
async progress (id: string, callback: (downloadStatus: DownloadStatus) => void) {
return new Promise((resolve, reject) => {
let gids = this.taskMap.get(id);
if (gids) {
let allStatus: DownloadStatus;
let subscription = this.updateEmitter.subscribe(() => {
try {
let status: DownloadStatus = new DownloadStatus();
// 合并每个状态信息
status =
gids!.map((value, index, array) => {
let s = this.downloadList.get(value);
if (!s) {
throw 'Gid not exists';
}
return s;
})
.reduce((previousValue, currentValue, currentIndex, array) => {
return previousValue.combine(currentValue);
}, status);
if (!allStatus) {
allStatus = status;
} else {
if (allStatus.compareTo(status) !== 0) {
allStatus = status;
}
}
if (allStatus.status === 'error') {
throw `Download Error: code ${allStatus.errorCode}, message: ${allStatus.errorMessage}`;
} else if (allStatus.status === 'complete') {
resolve(null);
subscription.unsubscribe();
} else {
callback(allStatus);
}
} catch (e) {
reject(e);
subscription.unsubscribe();
}
});
} else {
throw 'Try to access invalid download id';
}
});
}
async getFiles (id: string): Promise<string[]> {
let gids = this.taskMap.get(id)!;
let files: string[] = [];
for (let gid of gids) {
let file = await this.aria2.getFiles(gid);
files.push(file[0].path);
}
return files;
}
async addMetalink (metalink: string, library: string): Promise<string> {
let encodedMeta4 = Buffer.from((metalink)).toString('base64');
let gidList = await this.aria2.addMetalink(encodedMeta4, {dir: library});
let taskId = this.createId();
this.taskMap.set(taskId, gidList);
// 每次添加任务,刷新一下本地任务列表
await this.refreshDownloadList();
return taskId;
}
async addUri (url: string, destination: string): Promise<string> {
await this.open;
let taskId = this.createId();
let gid = await this.aria2.addUri([url], {dir: destination});
this.taskMap.set(taskId, [gid]);
return taskId;
}
async pause (id: string): Promise<void> {
await this.open;
try {
await this.aria2.pause(id);
} catch (e) {
}
}
}
:host {
display: flex;
height: 100%;
}
#right {
display: flex;
flex-direction: column;
flex-grow: 1;
}
#main {
display: flex;
flex-grow: 1;
}
#candy-wrapper {
background-color: #444;
height: 230px;
flex-shrink: 0;
position: relative;
}
roster {
width: 190px;
background-color: #f7f7f9;
flex-shrink: 0;
}
/*a {*/
/*display: block;*/
/*padding: 10px 20px 10px 20px;*/
/*}*/
/*a:focus, a:hover {*/
/*text-decoration: none;*/
/*}*/
/*.active {*/
/*background-color: #428bca;*/
/*}*/
/*.active > a {*/
/*color: #fff;*/
/*}*/
span {
margin: 12px 0 8px 8px;
color: #a7a7a7;
font-size: 14px;
display: block;
}
.actions {
margin-bottom: 1em;
}
.progress {
height: 1em;
width: 1em;
float: right;
margin: 5px;
position: relative;
}
.pie {
height: 100%;
width: 100%;
clip: rect(0, 1em, 1em, 0.5em);
left: 0;
position: absolute;
top: 0;
}
.half-circle {
height: 100%;
width: 100%;
border: 0.2em solid #3498db;
border-radius: 50%;
clip: rect(0, 0.5em, 1em, 0);
left: 0;
position: absolute;
top: 0;
}
.shadow {
height: 100%;
width: 100%;
border: 0.2em solid #bdc3c7;
border-radius: 50%;
}
.right-side {
display: none;
}
.half-circle {
/*border-color: #e74c3c;*/
border-color: rgb(0, 116, 217);
}
.left-side {
/*transform: rotate(1turn);*/
/*在前台用Angular填写*/
}
.second-half {
clip: rect(auto, auto, auto, auto);
}
.second-half > .right-side {
display: inherit;
transform: rotate(0.5turn);
}
.fa-spin {
margin: 5px;
color: #0275d8;
font-weight: bold;
float: right;
}
#nav-wrapper {
width: 190px;
height: 100%;
flex-shrink: 0;
background-color: #f7f7f9;
border-right: 1px solid #eee;
padding-left: 0;
padding-right: 0;
padding-top: 20px;
/*resize: horizontal;*/
position: relative;
}
nav {
height: 100%;
}
.sidebar .nav {
margin-bottom: 20px;
}
.sidebar .nav-item {
width: 100%;
}
.sidebar .nav-item + .nav-item {
margin-left: 0;
}
.sidebar .nav-link {
border-radius: 0;
}
.nav-link {
padding: .3em 1em;
color: black;
font-size: 15px;
}
.nav-link.active {
background-color: #ebf3f8;
color: #00a4d9;
}
a {
cursor: default;
}
.search {
background-color: #ebf3f8;
border: none;
}
i.search {
color: #a7a7a7;
}
input.search {
padding-left: 0;
font-size: 14px;
font-family: -apple-system, Arial, 'Source Sans Pro', "Microsoft YaHei", 'Microsoft JhengHei', "WenQuanYi Micro Hei", sans-serif;
}
input.search::-webkit-input-placeholder {
color: #a7a7a7;
}
#search {
padding: 0 10px;
}
.icon {
width: 16px;
height: 16px;
}
nav::-webkit-resizer {
/*width: 100px;*/
/*height: 100px;*/
/*background-color: red;*/
border: 2px solid yellow;
background: blue;
box-shadow: 0 0 2px 5px red;
outline: 2px dashed green;
/*size does not work*/
display: block;
width: 150px !important;
height: 150px !important;
position: fixed;
}
.resize-wrapper {
position: relative;
}
.resize {
position: absolute;
}
.resize-right .resize {
width: 4px;
right: -2px;
top: 0;
bottom: 0;
cursor: col-resize;
}
.resize-top .resize {
height: 4px;
top: -2px;
left: 0;
right: 0;
cursor: row-resize;
}
#nav-wrapper {
z-index: 90;
box-shadow: 0 0 5px rgba(0, 0, 0, .2);
}
#candy-wrapper {
z-index: 80;
box-shadow: 0 0 5px rgba(0, 0, 0, .2);
}
roster {
z-index: 90;
}
/*#right-shadow {*/
/*width: 190px;*/
/*z-index: 95;*/
/*box-shadow: 0 0 5px rgba(0, 0, 0, .2);*/
/*position: absolute;*/
/*top: 0;*/
/*right: 0;*/
/*bottom: 0;*/
/*}*/
<p>lobby works!</p>
<!-- Begin page content -->
<div #nav class='resize-wrapper resize-right' id='nav-wrapper'>
<nav *ngIf='apps' class='bg-faded sidebar scroll' id='apps'>
<!-- <div id="search" class="input-group">-->
<!-- <i class="fa fa-search input-group-addon search" id="basic-addon1"></i>-->
<!-- <input i18n-placeholder #search id="search-input" type="text" class="form-control search" placeholder="搜索游戏" aria-describedby="basic-addon1">-->
<!-- </div>-->
<span *ngIf='grouped_apps.installed' i18n>已安装</span>
<ul *ngIf='grouped_apps.installed' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.installed' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}<i *ngIf='!app.isReady() && !app.status.total'
class='spin fa fa-circle-o-notch fa-spin fa-fw'></i>
<div *ngIf='!app.isReady() && app.status.total' class='progress'>
<div [class.second-half]='app.status.progress/app.status.total>0.5' class='pie'>
<div [style.transform]="'rotate('+(app.status.progress/app.status.total).toString()+'turn)'"
class='left-side half-circle'></div>
<div class='right-side half-circle'></div>
</div>
<div class='shadow'></div>
</div>
</a>
</li>
</ul>
<span *ngIf='grouped_apps.test'>测试</span>
<ul *ngIf='grouped_apps.test' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.test' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}
</a>
</li>
</ul>
<span *ngIf='grouped_apps.recommend' i18n>推荐</span>
<ul *ngIf='grouped_apps.recommend' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.recommend' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}
</a>
</li>
</ul>
<span *ngIf='grouped_apps.ygopro' i18n>YGOPro 各发行版</span>
<ul *ngIf='grouped_apps.recommend' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.ygopro' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}
</a>
</li>
</ul>
<span *ngIf='grouped_apps.mysterious' i18n>迷之物体</span>
<ul *ngIf='grouped_apps.mysterious' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.mysterious' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}
</a>
</li>
</ul>
<span *ngIf='grouped_apps.touhou' i18n>东方 Project</span>
<ul *ngIf='grouped_apps.touhou' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.touhou' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}
</a>
</li>
</ul>
<span *ngIf='grouped_apps.touhou_pc98' i18n>东方旧作</span>
<ul *ngIf='grouped_apps.touhou_pc98' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.touhou_pc98' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}
</a>
</li>
</ul>
<span *ngIf='grouped_apps.runtime_installed' i18n>已安装的运行库</span>
<ul *ngIf='grouped_apps.runtime_installed' class='nav nav-pills flex-column'>
<li *ngFor='let app of grouped_apps.runtime_installed' class='nav-item'>
<a (click)='$event.preventDefault(); chooseApp(app)' [class.active]='app===currentApp' [href]="'https://mycard.moe/' + app.id"
class='nav-link'>
<img *ngIf='app.icon' [src]='app.icon' class='icon'>
{{app.name}}
</a>
</li>
</ul>
</nav>
<div (mousedown)='mousedown($event)' class='resize'></div>
</div>
<div id='right'>
<!-- <div id='main'>-->
<app-detail *ngIf='currentApp' [currentApp]='currentApp' class='scroll'></app-detail>
<!-- <roster class='scroll'></roster>-->
<!-- </div>-->
<div class='resize-wrapper resize-top' id='candy-wrapper' style='max-height: calc( 100% - 180px )'>
<div (mousedown)='mousedown($event)' class='resize'></div>
<candy *ngIf='currentApp' [currentApp]='currentApp'></candy>
</div>
</div>
<div id='right-shadow'></div>
import { Component, OnInit } from '@angular/core';
/**
* Created by zh99998 on 16/9/2.
*/
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { AppsService } from '../apps.service';
import { LoginService } from '../login/login.service';
import { App, Category } from '../shared/app';
import { shell } from 'electron';
import { SettingsService } from '../settings.service';
const ReconnectingWebSocket = require('reconnecting-websocket');
// import 'typeahead.js';
// import Options = Twitter.Typeahead.Options;
@Component({
selector: 'app-lobby',
templateUrl: './lobby.component.html',
styleUrls: ['./lobby.component.css']
selector: 'lobby',
templateUrl: 'lobby.component.html',
styleUrls: ['lobby.component.css']
})
export class LobbyComponent implements OnInit {
constructor() { }
currentApp: App;
resizing: HTMLElement | undefined;
offset: number;
@ViewChild('search')
search: ElementRef;
public apps: Map<string, App>;
//private messages: WebSocket;
constructor(private appsService: AppsService, private loginService: LoginService,
private settingsService: SettingsService, private ref: ChangeDetectorRef) {
}
get grouped_apps(): any {
// @ts-ignore
let contains = ['game', 'music', 'book'].map((value) => Category[value]);
let result = { runtime: [] };
for (let app of this.apps.values()) {
let tags: string[];
if (contains.includes(app.category)) {
if (app.isInstalled()) {
tags = ['installed'];
} else {
tags = app.tags || ['test'];
}
} else {
if (app.isInstalled()) {
tags = ['runtime_installed'];
} else {
tags = ['runtime'];
}
}
for (const tag of tags) {
if (!result[tag]) {
result[tag] = [];
}
result[tag].push(app);
}
}
return result;
}
async ngOnInit() {
this.apps = await this.appsService.loadApps();
if (this.apps.size > 0) {
this.chooseApp(this.appsService.lastVisited || this.apps.get('ygopro')!);
await this.appsService.migrate();
for (let app of this.apps.values()) {
await this.appsService.update(app);
}
} else {
if (confirm('获取程序列表失败,是否重试?')) {
location.reload();
} else {
window.close();
}
}
// 特化个 YGOPRO 国际服聊天室。其他的暂时没需求。
if (!this.settingsService.getLocale().startsWith('zh')) {
this.apps.get('ygopro')!.conference = 'ygopro-international';
}
this.ref.detectChanges();
/* let url = new URL('wss://sapi.moecube.com:444:3100');
let params: URLSearchParams = url.searchParams;
params.set('user_id', this.loginService.user.email);
this.messages = new ReconnectingWebSocket(url);
this.messages.onmessage = async(event) => {
let data = JSON.parse(event.data);
console.log(data);
this.apps = await this.appsService.loadApps();
this.currentApp = this.apps.get(this.currentApp.id)!;
}; */
// $(this.search.nativeElement).typeahead(<any>{
// minLength: 1,
// highlight: true
// }, {
// name: 'apps',
// source: (query, syncResults) => {
// query = query.toLowerCase();
// let result = Array.from(this.apps.values())
// .filter((app: App) => [Category.game, Category.music, Category.book].includes(app.category))
// .filter((app: App) => app.id.includes(query) || app.name.toLowerCase().includes(query))
// .map((app: App) => app.name);
// console.log(result);
// syncResults(result);
// }
// });
document.addEventListener('mousemove', (event: MouseEvent) => {
if (!this.resizing) {
return;
}
if (this.resizing.classList.contains('resize-right')) {
let width = this.offset + event.clientX;
if (width < 190) {
width = 190;
}
if (width > 400) {
width = 400;
}
this.resizing.style.width = `${width}px`;
} else {
let height = this.offset - event.clientY;
let main_height = event.clientY - document.getElementById('navbar')!.clientHeight;
// console.log(event.clientY);
if (height > 150 && main_height > 180) {
if (height < 230) {
height = 230;
}
this.resizing.style.height = `${height}px`;
if ($('#candy').attr('data-minormax') !== 'default') {
$('#candy').attr('data-minormax', 'default');
$('#mobile-roster-icon').css('display', 'block');
$('#chat-toolbar').css('display', 'block');
$('#chat-rooms').css('display', 'block');
$('#context-menu').css('display', 'block');
$('#mobile-roster-icon').css('display', 'block');
$('#minimize').show();
$('#unminimize').hide();
$('#restore').hide();
$('#maximize').show();
}
} else if (height <= 150) {
$('#candy').attr('data-minormax', 'min');
this.resizing.style.height = '31px';
$('#mobile-roster-icon').css('display', 'none');
$('#chat-toolbar').css('display', 'none');
$('#chat-rooms').css('display', 'none');
$('#context-menu').css('display', 'none');
$('#mobile-roster-icon').css('display', 'none');
$('#minimize').hide();
$('#unminimize').show();
$('#restore').hide();
$('#maximize').show();
} else if (main_height <= 180) {
$('#candy').attr('data-minormax', 'max');
this.resizing.style.height = 'calc( 100% - 180px )';
$('#minimize').show();
$('#unminimize').hide();
$('#restore').show();
$('#maximize').hide();
}
}
});
document.addEventListener('mouseup', (event: MouseEvent) => {
document.body.classList.remove('resizing');
this.resizing = undefined;
});
}
mousedown(event: MouseEvent) {
// console.log(()
document.body.classList.add('resizing');
this.resizing = <HTMLElement>(<HTMLElement>event.target).parentNode;
if (this.resizing.classList.contains('resize-right')) {
this.offset = this.resizing.offsetWidth - event.clientX;
} else {
this.offset = this.resizing.offsetHeight + event.clientY;
}
}
ngOnInit(): void {
chooseApp(app: App) {
this.currentApp = app;
this.appsService.lastVisited = app;
}
openExternal(url: string) {
shell.openExternal(url);
}
}
:host {
display: flex;
}
webview {
flex-grow: 1;
}
{{url | json}}
<!--<webview [src]="url" (will-navigate)="return_sso($event.url)" (new-window)="openExternal($event.url)"></webview>-->
<webview (new-window)='openExternal($event.url)' (will-navigate)='return_sso($event.url)' [src]='url'></webview>
......@@ -7,7 +7,7 @@ import crypto from 'crypto';
import { shell } from 'electron';
@Component({
selector: 'app-login',
selector: 'login',
templateUrl: 'login.component.html',
styleUrls: ['login.component.css']
})
......
<!--<div class="container">-->
<header class='navbar navbar-light bg-light'>
<a class='navbar-brand' href='#'>
<img class='me-2' src='assets/icon.ico'>
<span class='text-primary'>MyCard</span>
</a>
<ul class="nav me-auto">
<li *ngIf="!loginService.logged_in" class="nav-item active">
<a i18n class="nav-link" href="#">登录</a>
</li>
<!--<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'store'}" class="nav-item">-->
<!--<a (click)="currentPage = 'store'" class="nav-link" href="#">商店</a>-->
<!--</li>-->
<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'lobby'}" class="nav-item">
<a i18n (click)="currentPage='lobby'" class="nav-link" href="#">游戏</a>
</li>
<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'community'}" class="nav-item">
<a i18n (click)="currentPage='community'" class="nav-link" href="#">社区</a>
</li>
<!--<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'moesound'}" class="nav-item">-->
<!--<a i18n (click)="currentPage='moesound'" class="nav-link" href="#">萌音</a>-->
<!--</li>-->
<!--<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'about'}" class="nav-item">-->
<!--<a i18n (click)="currentPage='about'" class="nav-link" href="#">关于</a>-->
<!--</li>-->
</ul>
<div id="navbar-right" class='text-secondary'>
<span id="update-status">
<i #error [hidden]="update_status != 'error'" (click)="update_retry()" class="fa fa-exclamation-circle" data-bs-toggle="tooltip" i18n-title title="更新出错,点击重试"></i>
<i #checking_for_update [hidden]="update_status != 'checking-for-update'" class="fa fa-spinner fa-pulse fa-spin" data-bs-toggle="tooltip" i18n-title title="正在检查更新"></i>
<i #update_available [hidden]="update_status != 'update-available'" class="fa fa-refresh fa-spin" data-bs-toggle="tooltip" i18n-title title="正在下载更新"></i>
<i #update_downloaded [hidden]="update_status != 'update-downloaded'" (click)="update_install()" class="fa fa-angle-double-up" data-bs-toggle="tooltip" i18n-title title="下载更新完成,点击安装"></i>
</span>
<span id="user" *ngIf="loginService.logged_in">
<a href="#" class="profile"><img id="avatar" [src]="loginService.user.avatar_url" alt="image"></a>
<a href="#" class="profile item" id="username">{{loginService.user.username}}</a>
<i i18n (click)="loginService.logout()" class="fa fa-sign-out item-icon" aria-hidden="true" i18n-title title="切换用户"></i>
<i i18n data-bs-toggle="modal" data-bs-target="#settings-modal" class="fa fa-cog item-icon" aria-hidden="true" i18n-title title="设置"></i>
</span>
<span id="border">|</span>
<span id="window-buttons">
<i i18n (click)="currentWindow.minimize()" class="fa fa-minus" i18n-title title="最小化"></i>
<i i18n *ngIf="!currentWindow.isMaximized()" (click)="currentWindow.maximize()" class="fa fa-expand" i18n-title title="最大化"></i>
<i i18n *ngIf="currentWindow.isMaximized()" (click)="currentWindow.unmaximize()" class="fa fa-clone" i18n-title title="还原"></i>
<i i18n (click)="currentWindow.hide()" class="fa fa-times" i18n-title title="关闭"></i>
</span>
</div>
</header>
<app-login id='login' class="page" *ngIf="!loginService.logged_in"></app-login>
<!--<store class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'store'"></store>-->
<app-lobby class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'lobby'"></app-lobby>
<webview class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'community'" src="https://ygobbs.com" (new-window)="openExternal($event.url)"></webview>
<!--<about class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'about'"></about>-->
<!-- Modal -->
<div class="modal fade" id="settings-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 i18n class="modal-title" id="myModalLabel">MyCard 设置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form (submit)="submit()">
<div class="modal-body">
<div class="container">
<div class="form-group row">
<label i18n for="locale" class="col-sm-2 col-form-label">语言</label>
<div class="col-sm-10">
<select class="form-control" id="locale" [(ngModel)]="locale" name="locale">
<option value="en-US">English</option>
<option value="zh-CN">简体中文</option>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button i18n type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button i18n type="submit" class="btn btn-primary" data-bs-dismiss="modal">确定</button>
</div>
</form>
</div>
</div>
</div>
.navbar {
padding: initial;
}
.navbar-brand {
width: 200px;
font-size: 24px;
text-align: center;
img {
width: 28px;
vertical-align: text-bottom;
}
}
.nav {
.nav-link {
font-size: 18px;
color: #a7a7a7;
padding: .8rem 1.2em;
}
.nav-item.active {
background-color: white;
.nav-link {
color: #00a4d9;
}
}
}
#navbar-right {
display: flex;
#avatar {
height: 1.5rem;
}
.item {
display: block;
//float: left;
//padding-top: .425rem;
//padding-bottom: .425rem;
margin: 0 0.8rem;
text-decoration: none;
color: #a7a7a7;
}
}
/*:host {*/
/*background-color: white;*/
/*}*/
.page {
flex-grow: 1;
/*margin-bottom: 60px;*/
}
#avatar {
display: block;
float: left;
border-radius: 10%;
height: 1.5rem;
margin-top: 0.475rem;
}
.item {
display: block;
float: left;
padding-top: .425rem;
padding-bottom: .425rem;
margin: 0 0.8rem;
text-decoration: none;
color: #a7a7a7;
}
.item-icon {
color: #a7a7a7;
font-size: 18px;
margin: .6rem .3rem;
}
.item:hover, .item-icon:hover, .nav-link:hover {
color: #5e5e5e;
}
#update-status > i {
line-height: 1.5;
color: #5e5e5e;
padding-top: .425rem;
padding-bottom: .425rem;
}
a {
cursor: default;
}
/* https://github.com/electron/electron/issues/7661#event-827104990 */
lobby[hidden], webview[hidden] {
width: 0;
height: 0;
flex: 0 1;
display: inherit !important;
overflow: hidden;
}
/* 不加这个切到有 Webview 的页面,上面的圆角会消失 */
/* 即使加了这个,下面的圆角也会消失 */
/*webview {*/
/*overflow: hidden;*/
/*}*/
#navbar {
background-color: #f7f7f9 !important;
padding: 0;
}
#navbar-brand {
color: #00a4d9;
font-size: 24px;
width: 190px;
margin: 0;
text-align: center;
}
#navbar-brand img {
width: 24px;
margin-top: -5px;
}
#navbar .nav-link {
font-size: 18px;
color: #a7a7a7;
padding: .8rem 1.2em;
}
.nav-item.active {
background-color: white;
}
#navbar .nav-item.active .nav-link {
color: #00a4d9;
}
#border {
margin: .2rem 0.4rem;
color: #a7a7a7;
font-size: 1.2rem;
}
#navbar {
z-index: 100;
}
#settings-modal .modal-dialog {
margin-top: 50px;
}
<header class='navbar navbar-toggleable-md navbar-light' id='navbar'>
<a class='navbar-brand' href='#' id='navbar-brand'>
<img src='assets/icon.ico' /> MyCard
</a>
<ul class='nav me-auto'>
<li *ngIf='!loginService.logged_in' class='nav-item active'>
<a class='nav-link' href='#' i18n>登录</a>
</li>
<!--<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'store'}" class="nav-item">-->
<!--<a (click)="currentPage = 'store'" class="nav-link" href="#">商店</a>-->
<!--</li>-->
<li *ngIf='loginService.logged_in' [ngClass]="{active: currentPage === 'lobby'}" class='nav-item'>
<a (click)="currentPage='lobby'" class='nav-link' href='#' i18n>游戏</a>
</li>
<li *ngIf='loginService.logged_in' [ngClass]="{active: currentPage === 'community'}" class='nav-item'>
<a (click)="currentPage='community'" class='nav-link' href='#' i18n>社区</a>
</li>
<!--<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'moesound'}" class="nav-item">-->
<!--<a i18n (click)="currentPage='moesound'" class="nav-link" href="#">萌音</a>-->
<!--</li>-->
<!--<li *ngIf="loginService.logged_in" [ngClass]="{active: currentPage === 'about'}" class="nav-item">-->
<!--<a i18n (click)="currentPage='about'" class="nav-link" href="#">关于</a>-->
<!--</li>-->
</ul>
<div id='navbar-right'>
<div id='update-status'>
<i #error (click)='update_retry()' [hidden]="update_status != 'error'" class='fa fa-exclamation-circle'
data-bs-toggle='tooltip' i18n-title title='更新出错,点击重试'></i>
<i #checking_for_update [hidden]="update_status != 'checking-for-update'" class='fa fa-spinner fa-pulse fa-spin'
data-bs-toggle='tooltip' i18n-title title='正在检查更新'></i>
<i #update_available [hidden]="update_status != 'update-available'" class='fa fa-refresh fa-spin'
data-bs-toggle='tooltip' i18n-title title='正在下载更新'></i>
<i #update_downloaded (click)='update_install()' [hidden]="update_status != 'update-downloaded'"
class='fa fa-angle-double-up' data-bs-toggle='tooltip' i18n-title title='下载更新完成,点击安装'></i>
</div>
<div *ngIf='loginService.logged_in' id='user'>
<a class='profile' href='#'><img [src]='loginService.user.avatar_url' alt='image' id='avatar'></a>
<a class='profile item' href='#' id='username'>{{loginService.user.username}}</a>
<i (click)='loginService.logout()' aria-hidden='true' class='fa fa-sign-out item-icon' i18n i18n-title
title='切换用户'></i>
<i aria-hidden='true' class='fa fa-cog item-icon' data-bs-target='#settings-modal' data-bs-toggle='modal' i18n
i18n-title title='设置'></i>
</div>
<div id='border'>|</div>
<div id='window-buttons'>
<i (click)='currentWindow.minimize()' class='fa fa-minus' i18n i18n-title title='最小化'></i>
<i (click)='currentWindow.maximize()' *ngIf='!currentWindow.isMaximized()' class='fa fa-expand' i18n i18n-title
title='最大化'></i>
<i (click)='currentWindow.unmaximize()' *ngIf='currentWindow.isMaximized()' class='fa fa-clone' i18n i18n-title
title='还原'></i>
<i (click)='currentWindow.hide()' class='fa fa-times' i18n i18n-title title='关闭'></i>
</div>
</div>
</header>
<login *ngIf='!loginService.logged_in' class='page'></login>
<store *ngIf='loginService.logged_in' [hidden]="currentPage != 'store'" class='page'></store>
<lobby *ngIf='loginService.logged_in' [hidden]="currentPage != 'lobby'" class='page'></lobby>
<webview (new-window)='openExternal($event.url)' *ngIf='loginService.logged_in' [hidden]="currentPage != 'community'" class='page'
src='https://ygobbs.com'></webview>
<!--<webview #moesound preload="./moesound.js" class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'moesound'" src="http://moesound.com/" (new-window)="moesound_newwindow($event.url)" (did-finish-load)="moesound_loaded()"></webview>-->
<about *ngIf='loginService.logged_in' [hidden]="currentPage != 'about'" class='page'></about>
<!-- Modal -->
<div aria-hidden='true' aria-labelledby='myModalLabel' class='modal fade' id='settings-modal' role='dialog'
tabindex='-1'>
<div class='modal-dialog' role='document'>
<div class='modal-content'>
<div class='modal-header'>
<h5 class='modal-title' i18n id='myModalLabel'>MyCard 设置</h5>
<button aria-label='Close' class='btn-close' data-bs-dismiss='modal' type='button'></button>
</div>
<form (submit)='submit()'>
<div class='modal-body'>
<div class='container'>
<div class='form-group row'>
<label class='col-sm-2 col-form-label' for='locale' i18n>语言</label>
<div class='col-sm-10'>
<select [(ngModel)]='locale' class='form-control' id='locale' name='locale'>
<option value='en-US'>English</option>
<option value='zh-CN'>简体中文</option>
</select>
</div>
</div>
</div>
</div>
<div class='modal-footer'>
<button class='btn btn-secondary' data-bs-dismiss='modal' i18n type='button'>取消</button>
<button class='btn btn-primary' data-bs-dismiss='modal' i18n type='submit'>确定</button>
</div>
</form>
</div>
</div>
</div>
import { LoginService } from './login/login.service';
import { SettingsService } from './settings.service';
import { Tooltip } from 'bootstrap';
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { app, getCurrentWindow, getGlobal, shell } from '@electron/remote';
import Mousetrap from 'mousetrap';
import { shell } from 'electron';
import * as remote from '@electron/remote';
import $ from 'jquery';
import { LoginService } from '../login/login.service';
import { SettingsService } from '../settings.service';
import { Tooltip } from 'bootstrap';
const autoUpdater: Electron.AutoUpdater = getGlobal('autoUpdater');
const autoUpdater: Electron.AutoUpdater = remote.getGlobal('autoUpdater');
const konami_code_logger: string[] = [];
@Component({
selector: 'app-root',
selector: 'mycard',
templateUrl: 'mycard.component.html',
styleUrls: ['mycard.component.scss']
styleUrls: ['mycard.component.css'],
})
export class MyCardComponent implements OnInit {
currentPage: string = 'lobby';
update_status: string | undefined = getGlobal('update_status');
update_status: string | undefined = remote.getGlobal('update_status');
update_error: string | undefined;
currentWindow = getCurrentWindow();
currentWindow = remote.getCurrentWindow();
window = window;
@ViewChild('error')
error: ElementRef;
......@@ -28,7 +30,7 @@ export class MyCardComponent implements OnInit {
update_available: ElementRef;
@ViewChild('update_downloaded')
update_downloaded: ElementRef;
update_elements: Map<string, ElementRef<HTMLElement>>;
update_elements: Map<string, ElementRef>;
locale: string;
......@@ -38,7 +40,44 @@ export class MyCardComponent implements OnInit {
moesound: ElementRef;
lastTooltip: Tooltip;
ngOnInit() {
this.update_elements = new Map(
Object.entries({
error: this.error,
'checking-for-update': this.checking_for_update,
'update-available': this.update_available,
'update-downloaded': this.update_downloaded,
})
);
$('#settings-modal').on('keyup', (event) => {
konami_code_logger.unshift(event.key);
if (
konami_code_logger[9] == 'ArrowUp' &&
konami_code_logger[8] == 'ArrowUp' &&
konami_code_logger[7] == 'ArrowDown' &&
konami_code_logger[6] == 'ArrowDown' &&
konami_code_logger[5] == 'ArrowLeft' &&
konami_code_logger[4] == 'ArrowRight' &&
konami_code_logger[3] == 'ArrowLeft' &&
konami_code_logger[2] == 'ArrowRight' &&
konami_code_logger[1].toLowerCase() == 'b' &&
konami_code_logger[0].toLowerCase() == 'a'
) {
this.currentWindow.webContents.openDevTools();
}
});
// document.addEventListener('drop', (event)=>{
// console.log('drop', event);
// event.preventDefault();
//
// });
}
constructor(public loginService: LoginService, private ref: ChangeDetectorRef, private settingsService: SettingsService) {
// renderer.listenGlobal('window', 'message', (event) => {
// console.log(event);
// // Do something with 'event'
// });
this.currentWindow.on('maximize', () => this.ref.detectChanges());
this.currentWindow.on('unmaximize', () => this.ref.detectChanges());
......@@ -61,19 +100,6 @@ export class MyCardComponent implements OnInit {
});
this.locale = this.settingsService.getLocale();
}
ngOnInit() {
this.update_elements = new Map(Object.entries({
'error': this.error,
'checking-for-update': this.checking_for_update,
'update-available': this.update_available,
'update-downloaded': this.update_downloaded
}));
// https://www.electronjs.org/docs/tutorial/keyboard-shortcuts#%E4%BD%BF%E7%94%A8%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93
Mousetrap.bind('up up down down left right left right b a', () => this.currentWindow.webContents.openDevTools());
}
update_retry() {
......@@ -86,7 +112,9 @@ export class MyCardComponent implements OnInit {
set_update_status(status: string) {
console.log('autoUpdater', status);
if (this.lastTooltip) this.lastTooltip.dispose();
if (this.lastTooltip) {
this.lastTooltip.dispose();
}
this.update_status = status;
this.ref.detectChanges();
......@@ -102,8 +130,8 @@ export class MyCardComponent implements OnInit {
submit() {
if (this.locale !== this.settingsService.getLocale()) {
localStorage.setItem(SettingsService.SETTING_LOCALE, this.locale);
app.relaunch();
app.quit();
remote.app.relaunch();
remote.app.quit();
}
}
......
#network {
display: inline-block;
vertical-align: middle;
width: 230px;
}
#network .input-group-btn > .btn:not(:last-child):not(.dropdown-toggle) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
#network .input-group-btn > .dropdown-toggle {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
<div id="network" *ngIf="currentApp.network && currentApp.network.protocol == 'maotama'">
<div class="input-group input-group-sm">
<input *ngIf="appsService.connections.get(currentApp)" [value]="appsService.connections.get(currentApp)!.address || 'Loading...'" readonly type="text" class="form-control" title="address">
<button i18n *ngIf="!appsService.connections.get(currentApp)" [disabled]="!appsService.allReady(currentApp)" (click)="appsService.network(currentApp, currentApp.network.servers[0])" type="button" class="btn btn-outline-secondary btn-sm">联机</button>
<button i18n *ngIf="appsService.connections.get(currentApp)" (click)="copy(appsService.connections.get(currentApp)!.address!)" [disabled]="!appsService.connections.get(currentApp)!.address" type="button" class="btn btn-outline-secondary btn-sm">复制</button>
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
<div class="dropdown-menu" [class.dropdown-menu-right]="appsService.connections.get(currentApp)">
<h6 i18n class="dropdown-header">选择服务器</h6>
<a *ngFor="let server of currentApp.network.servers" (click)="appsService.network(currentApp, server)" class="dropdown-item" href="#">{{server.id}}</a>
<div *ngIf="appsService.connections.get(currentApp)" class="dropdown-divider"></div>
<a i18n *ngIf="appsService.connections.get(currentApp)" (click)="appsService.connections.get(currentApp)!.connection.close()" class="dropdown-item" href="#">取消</a>
</div>
</div>
</div>
import {clipboard} from 'electron';
import {Component, Injectable, Input} from '@angular/core';
import {AppsService} from '../apps.service';
import {App} from '../shared/app';
@Component({
selector: 'network',
templateUrl: 'network.component.html',
styleUrls: ['network.component.css'],
})
@Injectable()
export class NetworkComponent {
@Input()
currentApp: App;
constructor(public appsService: AppsService) {
console.log('constructor');
}
copy(text: string) {
clipboard.writeText(text);
}
}
/**
* Created by weijian on 2016/12/5.
*/
export class ComparableSet<T> extends Set<T> {
constructor(values?: Iterable<T>) {
if (values) {
super(values);
} else {
super();
}
}
isSuperset(subset: Set<T>) {
for (let elem of subset) {
if (!this.has(elem)) {
return false;
}
}
return true;
}
union(setB: Set<T>): Set<T> {
let union = new Set(this);
for (let elem of setB) {
union.add(elem);
}
return union;
}
intersection(setB: Set<T>): Set<T> {
let intersection = new Set<T>();
for (let elem of setB) {
if (this.has(elem)) {
intersection.add(elem);
}
}
return intersection;
}
difference(setB: Set<T>): Set<T> {
let difference = new Set(this);
for (let elem of setB) {
difference.delete(elem);
}
return difference;
}
}
import {App} from './app';
/**
* Created by zh99998 on 16/9/6.
*/
export class AppLocal {
path: string;
version: string;
files: Map<string, string>;
action: Map<string, {interpreter?: string, execute: string, args: string[], env: {}, open: App}>;
update(local: any) {
this.path = local.path;
this.version = local.version;
let files = new Map<string, string>();
for (let filename of Object.keys(local.files)) {
files.set(filename, local.files[filename]);
}
this.files = files;
}
toJSON() {
let t: any = {};
for (let [k, v] of this.files) {
t[k] = v;
}
return {path: this.path, version: this.version, files: t};
}
}
import { AppLocal } from './app-local';
import path from 'path';
import ini from 'ini';
import fs from 'fs';
import child_process from 'child_process';
import Mustache from 'mustache';
export enum Category {
game,
music,
book,
runtime,
emulator,
language,
expansion,
module
}
// export type CategoryString = 'game' | 'music' | 'book' | 'runtime' | 'emulator' | 'language' | 'expansion' | 'module'
// export enum DownloadStatus{
// downloading,
// init,
// installing,
// ready,
// updating,
// uninstalling,
// waiting,
// }
export interface BaseAction {
execute: string;
args: string[];
env: {};
}
export interface Action extends BaseAction {
interpreter?: string;
open?: App;
}
export interface SpawnAction extends BaseAction {
cwd?: string;
}
export class FileOptions {
sync: boolean;
ignore: boolean;
}
export class AppStatus {
progress: number;
total: number;
private _status: string;
get status(): string {
return this._status;
}
set status(status: string) {
this.progress = 0;
this.total = 0;
this.progressMessage = '';
this._status = status;
}
progressMessage: string;
}
export interface YGOProDistroData {
deckPath: string;
replayPath: string;
systemConf: string;
}
export class App {
id: string;
name: string; // i18n
description: string; // i18n
author: string; // English Only
homepage: string;
developers: { name: string, url: string }[];
released_at: Date;
updated_at: Date;
category: Category;
parent?: App;
actions: Map<string, Action>;
references: Map<string, App>;
dependencies: Map<string, App>;
locales: string[];
news: any[];
network: any;
tags: string[];
version: string;
local: AppLocal | null;
status: AppStatus;
conference: string | undefined;
files: Map<string, FileOptions>;
data: any;
icon: string;
cover: string;
background: string;
price: { [currency: string]: string };
key?: string;
static getQuerySuffix(platform: string, locale: string, arch: string) {
const params = new URLSearchParams();
params.set('platform', platform);
params.set('locale', locale);
params.set('arch', arch);
return params.toString();
}
static downloadUrl(app: App, platform: string, locale: string, arch: string): string {
/*
if (app.id === 'ygopro') {
return `https://sthief.moecube.com:444/metalinks/${app.id}-${process.platform}-${locale}/${app.version}`;
} else if (app.id === 'desmume') {
return `https://sthief.moecube.com:444/metalinks/${app.id}-${process.platform}/${app.version}`;
}
return `https://sthief.moecube.com:444/metalinks/${app.id}/${app.version}`;
*/
return `https://sapi.moecube.com:444/release/update/metalinks/${app.id}/${app.version}?${this.getQuerySuffix(platform, locale, arch)}`;
}
static checksumUrl(app: App, platform: string, locale: string, arch: string): string {
/*if (app.id === 'ygopro') {
return `https://sthief.moecube.com:444/checksums/${app.id}-${platform}-${locale}/${app.version}`;
} else if (app.id === 'desmume') {
return `https://sthief.moecube.com:444/checksums/${app.id}-${platform}/${app.version}`;
}
return `https://sthief.moecube.com:444/checksums/${app.id}/${app.version}`;*/
return `https://sapi.moecube.com:444/release/update/checksums/${app.id}/${app.version}?${this.getQuerySuffix(platform, locale, arch)}`;
}
static updateUrl(app: App, platform: string, locale: string, arch: string): string {
/*if (app.id === 'ygopro') {
return `https://sthief.moecube.com:444/update/${app.id}-${platform}-${locale}/${app.version}`;
} else if (app.id === 'desmume') {
return `https://sthief.moecube.com:444/update/${app.id}-${platform}/${app.version}`;
}*/
return `https://sapi.moecube.com:444/release/update/update/${app.id}/${app.version}?${this.getQuerySuffix(platform, locale, arch)}`;
}
isBought(): Boolean {
// 免费或有 Key
return !this.price || !!this.key;
}
isLanguage() {
return this.category === Category.module && this.tags.includes('language');
}
reset() {
this.status.status = 'init';
this.local = null;
localStorage.removeItem(this.id);
}
isInstalled(): boolean {
return this.status.status !== 'init';
}
isReady(): boolean {
return this.status.status === 'ready';
}
isInstalling(): boolean {
return this.status.status === 'installing';
}
isWaiting(): boolean {
return this.status.status === 'waiting';
}
isDownloading(): boolean {
return this.status.status === 'downloading';
}
isUninstalling(): boolean {
return this.status.status === 'uninstalling';
}
isUpdating(): boolean {
return this.status.status === 'updating';
}
runnable(): boolean {
return [Category.game].includes(this.category);
}
progressMessage(): string | undefined {
return this.status.progressMessage;
}
constructor(app: any) {
this.id = app.id;
this.name = app.name;
this.description = app.description;
this.developers = app.developers;
this.released_at = app.released_at;
this.updated_at = app.updated_at;
this.author = app.author;
this.homepage = app.homepage;
this.category = Category[<string>app.category];
this.actions = app.actions;
this.dependencies = app.dependencies;
this.parent = app.parent;
this.references = app.references;
this.locales = app.locales;
this.news = app.news;
this.network = app.network;
this.tags = app.tags;
this.version = app.version;
this.conference = app.conference;
this.files = app.files;
this.data = app.data;
this.icon = app.icon;
this.cover = app.cover;
this.background = app.background;
this.price = app.price;
this.key = app.key;
}
findDependencies(): App[] {
if (this.dependencies && this.dependencies.size > 0) {
let set = new Set<App>();
for (let dependency of this.dependencies.values()) {
dependency.findDependencies()
.forEach((value) => {
set.add(value);
});
set.add(dependency);
}
return Array.from(set);
}
return [];
}
readyForInstall(): boolean {
let dependencies = this.findDependencies();
return dependencies.every((dependency) => dependency.isReady());
}
async getSpawnAction(children: App[], action_name = 'main', referencedApp?: App, referencedAction?: Action, cwd?: string, argsTemplate?: any): Promise<SpawnAction> {
const appCwd = (<AppLocal>this.local).path;
if (!cwd) {
cwd = appCwd;
}
if (this.id === 'np2fmgen') {
const config_file = path.join(this.local!.path, 'np21nt.ini');
let config = await new Promise<Record<string, any>>((resolve, reject) => {
fs.readFile(config_file, { encoding: 'utf-8' }, (error, data) => {
if (error) {
return reject(error);
}
resolve(ini.parse(data));
});
});
const default_config = {
clk_mult: '48',
DIPswtch: '3e f3 7b',
SampleHz: '44100',
Latencys: '100',
MIX_TYPE: 'true',
windtype: '0'
};
config['NekoProject21'] = Object.assign({}, default_config, config['NekoProject21']);
config['NekoProject21']['HDD1FILE'] =
path.win32.join(process.platform === 'win32' ? '' : 'Z:', referencedApp!.local!.path, referencedAction!.execute);
config['NekoProject21']['fontfile'] =
path.win32.join(process.platform === 'win32' ? '' : 'Z:', referencedApp!.local!.path, 'font.bmp');
await new Promise((resolve, reject) => {
fs.writeFile(config_file, ini.stringify(config), (error) => {
if (error) {
reject(error);
} else {
resolve(null);
}
});
});
cwd = appCwd;
}
let action: Action = <Action>this.actions.get(action_name);
let args: string[] = [];
let env = Object.assign({}, process.env);
for (let child of children) {
if (child.isInstalled()) {
let _action = child.actions.get(action_name);
if (_action) {
action = _action;
}
}
}
let execute: string;
const appExecute = path.join(appCwd, action.execute);
if (action.interpreter) {
execute = action.interpreter;
args.push(appExecute);
} else {
execute = appExecute;
}
if (action.open) {
const np2 = action.open;
const openAction = await np2.getSpawnAction([], 'main', this, action, cwd, argsTemplate);
args = args.concat(openAction.args);
args.push(action.execute);
execute = openAction.execute;
cwd = openAction.cwd;
}
args = args.concat(action.args);
if (argsTemplate) {
for (let i = 0; i < args.length; ++i) {
if (typeof args[i] !== 'string') {
continue;
}
args[i] = Mustache.render(args[i], argsTemplate, undefined, { escape: (v) => v });
}
}
env = Object.assign(env, action.env);
return {
execute,
args,
env,
cwd
};
}
async spawnApp(children: App[], action_name = 'main', argsTemplate?: any) {
if (this.id === 'th123') {
let th105 = <App>this.references.get('th105');
if (th105.isInstalled()) {
console.log(`Reference of th123: ${th105}`);
const config_file = path.join((<AppLocal>this.local).path, 'configex123.ini');
let config = ini.parse(await fs.promises.readFile(config_file, { encoding: 'utf-8' }));
const th105LocalApp = (<AppLocal>th105.local);
const targetTh105Path = th105LocalApp ? th105LocalApp.path : (<AppLocal>this.local).path.replace(/th123/g, 'th105');
config['th105path'] = { path: targetTh105Path };
await fs.promises.writeFile(config_file, ini.stringify(config));
}
}
const appCwd = (<AppLocal>this.local).path;
const { execute, args, env, cwd } = await this.getSpawnAction(children, action_name, undefined, undefined, undefined, argsTemplate);
console.log(execute, args, env, cwd, appCwd);
return child_process.spawn(execute, args, { env: env, cwd: cwd || appCwd });
}
get isYGOPro(): boolean {
return !!this.ygoproDistroData;
}
get ygoproDistroData(): YGOProDistroData | undefined {
if (!this.data) {
return;
}
return this.data.ygopro || undefined;
}
get ygoproDeckPath(): string | undefined {
const distroData = this.ygoproDistroData;
if (!distroData) {
return;
}
return path.join(this.local!.path, distroData.deckPath);
}
get ygoproReplayPath(): string | undefined {
const distroData = this.ygoproDistroData;
if (!distroData || !distroData.replayPath) {
return;
}
return path.join(this.local!.path, distroData.replayPath);
}
get systemConfPath(): string | undefined {
const distroData = this.ygoproDistroData;
if (!distroData || !distroData.systemConf) {
return;
}
return path.join(this.local!.path, distroData.systemConf);
}
}
export namespace AppsJson {
export enum Locale {
zh_CN = 'zh-CN',
en_US = 'en-US',
ja_JP = 'ja-JP',
ko_KR = 'ko-KR',
pt_BR = 'pt-BR',
zh_HK = 'zh-HK',
zh_TW = 'zh-TW',
}
export enum Platform {
Linux = 'linux',
macOS = 'darwin',
Windows = 'win32',
}
export type LocaleWise<T> = Record<Locale, T>;
export type PlatformWise<T> = Record<Platform, T>;
export interface Developer {
name: string;
url: string;
}
export interface Trailer {
type: string;
url: string;
url2?: string;
}
export interface Action {
interpreter?: string;
execute: string;
args: any[];
env: Record<string, string>;
open?: string;
}
export type PlatformAction = Record<string, Action>;
export interface News {
url: string;
image: string;
title: string;
text: string;
updated_at: string;
}
export interface NetworkServer {
id: string;
url: string;
}
export interface Network {
protocol: string;
port: number;
servers: NetworkServer[];
}
export interface Syncable {
sync: boolean;
}
export interface Price {
cny: number;
usd: number;
}
export interface App {
id: string;
key?: string;
name?: LocaleWise<string>;
description?: LocaleWise<string>;
developers?: LocaleWise<Developer[]>;
publishers?: LocaleWise<Developer[]>;
released_at?: string;
category?: string;
tags?: string[];
trailer?: Trailer[];
dependencies?: PlatformWise<string[]>;
references?: PlatformWise<string[]>;
author?: string;
homepage?: string;
locales?: string[];
actions?: PlatformWise<PlatformAction>;
version?: PlatformWise<string>;
news?: LocaleWise<News[]>;
conference?: string;
icon?: string;
cover?: string;
background?: string;
parent?: string;
network?: Network;
updated_at?: string;
files?: Record<string, Syncable>;
data?: any;
price?: Price;
}
}
/**
* Created by break on 2017/6/9.
*/
import $ from 'jquery';
let data_url = (new URL(document.location.toString())).searchParams;
let data_str = data_url.get('data');
// {
// "usernamea": "Joe1991",
// "usernameb": "zh99998",
// "userscorea": 1,
// "userscoreb": 2,
// "expa": 1,
// "expb": 30,
// "expa_ex": 0.5,
// "expb_ex": 29,
// "pta": -2.45677803214143,
// "ptb": 562.760086898395,
// "pta_ex": -1.25048918195558,
// "ptb_ex": 561.553798048209,
// "type": "athletic",
// "start_time": "2017-06-17T12:26:33.000Z",
// "end_time": "2017-06-17T12:26:33.000Z",
// "winner": "zh99998",
// "isfirstwin": false,
// "myname":"zh99998",
// "athletic_win":23,
// "athletic_lose":0,
// "entertain_win":7,
// "entertain_lose":0,
// "exp_rank":"1685",
// "arena_rank":"335",
// "exp_rank_ex":"1685",
// "arena_rank_ex":"335",
// }
let data = JSON.parse(data_str!);
let titleStr;
let icon = 'https://ygobbs.com/user_avatar/ygobbs.com/' + data.myname + '/120/1.png';
let myMame = data.myname;
let winTimes, loseTimes, rank, rank_up, DP, DP_up, DP_up_sum, EXP, EXP_up;
let winOrLose = 0;
let isMyFirstWin;
if (data.type === 'entertain') {
titleStr = '娱乐匹配';
winTimes = data.entertain_win;
loseTimes = data.entertain_lose;
rank = data.exp_rank;
rank_up = data.exp_rank_ex - data.exp_rank ;
} else {
titleStr = '竞技匹配';
winTimes = data.athletic_win;
loseTimes = data.athletic_lose;
rank = data.arena_rank;
rank_up = data.arena_rank_ex - data.arena_rank ;
}
if (data.usernamea === data.myname) {
if ( data.userscorea > data.userscoreb) {
winOrLose = 1;
}else if ( data.userscorea < data.userscoreb) {
winOrLose = -1;
}else {
winOrLose = 0;
}
DP = parseInt(data.pta);
DP_up_sum = Math.floor( data.pta - data.pta_ex );
EXP = parseInt(data.expa);
EXP_up = Math.floor( data.expa - data.expa_ex );
}else {
if ( data.userscorea < data.userscoreb) {
winOrLose = 1;
}else if ( data.userscorea > data.userscoreb) {
winOrLose = -1;
}else {
winOrLose = 0;
}
DP = parseInt(data.ptb);
DP_up_sum = Math.floor( data.ptb - data.ptb_ex );
EXP = parseInt(data.expb);
EXP_up = Math.floor( data.expb - data.expb_ex );
}
isMyFirstWin = (winOrLose > 0 && data.isfirstwin) ? true : false;
DP_up = DP_up_sum - (isMyFirstWin ? 4 : 0);
// =========================================================================
$('#title').html(titleStr);
$('#icon').attr('src', icon);
$('#myName').html(myMame);
$('#' + (winOrLose ? (winOrLose > 0 ? 'win' : 'lose') : 'draw') ).show();
let tr1 = '<tr>' +
'<td>胜:<span class="' + (winOrLose > 0 ? 'green' : '') + '">' + winTimes + '</span></td>' +
'<td>负:<span class="' + (winOrLose < 0 ? 'red' : '') + '">' + loseTimes + '</span></td>' +
'</tr>';
let tr2 = `<tr>
<td>排名:<span id="rank" class="${rank_up ? (rank_up > 0 ? 'green' : 'red') : '' } ">${rank}</span></td>
<td>${data.type === 'entertain' ? 'EXP:' : 'DP:'}
<span id="EXP_DP" ${data.type === 'entertain' ?
('class="' + (EXP_up > 0 ? 'green' : (EXP_up < 0 ? 'red' : '')) + '">' + EXP) :
('class="' + (DP_up_sum > 0 ? 'green' : (DP_up_sum < 0 ? 'red' : '')) + '">' + DP)
}</span>
</td>
</tr>`;
$('#info').append(tr1).append(tr2);
let tr_DP = DP_up ? `
<tr>
<td>D.P</td>
${DP_up > 0 ? `<td class="green">+${ DP_up }</td>` : `<td class="red">${ DP_up }</td>`}
</tr>
` : ``;
let tr_EXP = EXP_up ? `
<tr>
<td>EXP</td>
${EXP_up > 0 ? `<td class="green">+${ EXP_up }</td>` : `<td class="red">${ EXP_up }</td>`}
</tr>
` : ``;
let tr_FirstWin = isMyFirstWin ? `
<tr>
<td>首胜</td>
<td class="green">+4</td>
</tr>
` : ``
let tr_rewards = tr_EXP + tr_DP + tr_FirstWin;
tr_rewards = tr_rewards === '' ? '<tr><td>无</td></tr>' : tr_rewards;
$('#rewards').append(tr_rewards);
function again() {
let {ipcRenderer} = require('electron');
ipcRenderer.send('YGOPro', data.type);
window.opener=null;
window.close();
}
let t = setTimeout(function () {
window.opener = null;
window.close();
}, 5000);
$('html').hover(function () {
clearTimeout(t);
});
/**
* Created by zh99998 on 2017/6/1.
*/
// import Raven from 'raven-js';
// import {ErrorHandler} from '@angular/core';
//
// Raven
// .config('https://2c5fa0d0f13c43b5b96346f4eff2ea60@sentry.io/174769')
// .install();
//
// export class RavenErrorHandler implements ErrorHandler {
// handleError(err: any): void {
// Raven.captureException(err.originalError || err);
// }
// }
import {LOCALE_ID, TRANSLATIONS, TRANSLATIONS_FORMAT} from '@angular/core';
import * as remote from '@electron/remote';
export async function getTranslationProviders (): Promise<Object[]> {
let locale = localStorage.getItem('locale');
if (!locale) {
locale = remote.app.getLocale();
localStorage.setItem('locale', locale);
}
const noProviders: Object[] = [];
if (!locale || locale === 'zh-CN') {
return noProviders;
}
const translationFile = `./locale/messages.${locale}.xlf`;
try {
let translations = await getTranslationsWithSystemJs(translationFile);
return [
{provide: TRANSLATIONS, useValue: translations},
{provide: TRANSLATIONS_FORMAT, useValue: 'xlf'},
{provide: LOCALE_ID, useValue: locale}
];
} catch (error) {
return noProviders;
}
}
declare const System: any;
function getTranslationsWithSystemJs (file: string) {
return System.import(file + '!text'); // relies on text plugin
}
import {App} from './app';
import path from 'path';
/**
* Created by weijian on 2016/10/24.
*/
export class InstallOption {
app: App;
downloadFiles: string[];
installLibrary: string;
get installDir (): string {
return path.join(this.installLibrary, this.app.id);
}
createShortcut: boolean;
createDesktopShortcut: boolean;
constructor (app: App, installLibrary = '', shortcut = false, desktopShortcut = false) {
this.app = app;
this.createShortcut = shortcut;
this.createDesktopShortcut = desktopShortcut;
this.installLibrary = installLibrary;
}
}
/*#game-list-modal tbody {*/
/*display: block;*/
/*overflow-y: auto;*/
/*height: 21.5rem;*/
/*}*/
/*#game-list-modal thead {*/
/*position: relative;*/
/*display: block;*/
/*}*/
/*#game-list-modal tr {*/
/*width: 100%;*/
/*display: table;*/
/*}*/
/*#game-list-modal .table {*/
/*margin-bottom: 0;*/
/*}*/
/*#game-list-modal .close {*/
/*position: absolute;*/
/*top: 15px;*/
/*right: 15px;*/
/*}*/
/*#game-list-modal .modal-header {*/
/*padding-left: 0;*/
/*padding-right: 0;*/
/*}*/
/*#game-list-modal .modal-header th {*/
/*line-height: 36px;*/
/*padding-top: 0;*/
/*padding-bottom: 0;*/
/*border: none;*/
/*}*/
/*#game-list-modal .modal-body {*/
/*padding: 0;*/
/*height: 21.4rem;*/
/*}*/
/*#game-list-modal .modal-body tr:first-child td {*/
/*border-top: none;*/
/*}*/
/*#game-list-modal .modal-body tr:last-child td {*/
/*border-bottom: none;*/
/*}*/
/*.float-left {*/
/*float: left;*/
/*}*/
/*.actions {*/
/*margin-bottom: 1em;*/
/*}*/
.small-gutters {
margin-right: -5px;
margin-left: 0px;
}
.small-gutters > .col, .small-gutters > [class*="col-"] {
padding-right: 5px;
padding-left: 0px;
}
.small-gutters > .col-sm-4 {
flex: 0 0 40%;
max-width: 40%;
}
.small-gutters > .col-sm-8 {
flex: 0 0 60%;
max-width: 60%;
}
dl {
margin-bottom: 0;
}
/*.modal-dialog {*/
/*max-width: 600px;*/
/*}*/
/*label {*/
/*font-size: 15px;*/
/*}*/
form {
padding-bottom: .75rem;
}
.btn-primary {
background-color: #00a4d9;
border-color: #008dbb;
}
#action, #match-time {
margin-bottom: .5rem;
}
#match-time .input-group-addon {
display: block;
}
#game-create-windbot .modal-body {
max-height: 400px;
overflow-y: auto;
}
#game-replay-modal .modal-dialog,
#game-create-windbot .modal-dialog,
#game-list-modal .modal-dialog {
margin-top: 50px;
}
#game-list-modal .modal-content {
flex-direction: row;
}
#game-list-modal .table {
font-size: 14px;
}
#game-list-modal .avatar {
width: 18px;
height: 18px;
}
#game-list-modal form {
position: relative;
background-color: #f7f7f7;
border-left: 1px solid #eee;
width: 200px;
flex-shrink: 0;
}
#game-list-modal .modal-content {
overflow: hidden;
}
#game-list-modal #game-create-actions {
position: absolute;
right: 0;
bottom: .75rem;
}
#game-list-modal fieldset label:last-child {
margin-bottom: 0;
}
#game-list {
position: relative;
flex-grow: 1;
}
#game-list-modal h3 {
border-bottom: 2px solid #eceeef;
padding: .75rem;
vertical-align: top;
font-size: 15px;
font-weight: bold;
line-height: 1.5;
}
#game-list-modal form > div, #game-list-modal form > fieldset {
padding: 0 .75rem;
font-size: 14px;
}
#game-list-close {
position: absolute;
right: .75rem;
top: .5rem;
}
#game-list-modal th {
white-space: nowrap
}
#game-list-modal legend {
font-size: 14px;
}
#game-list-modal fieldset label {
padding-right: 0;
}
#game-replay-modal .nav-tabs .nav-item {
font-size: 15px;
}
#game-replay-bilibili webview {
height: 440px;
}
#game-replay-watch {
height: 480px;
}
#game-replay-watch table {
font-size: 14px;
}
#game-replay-watch th.mode {
width: 15%;
}
#game-replay-watch th.users {
width: 15%;
}
#game-replay-watch th.extra {
width: 20%;
}
#game-list th.users {
width: 20%;
}
#game-list th.mode {
width: 15%;
}
#game-list th.extra {
width: 35%;
}
#game-list td.users,
#game-list td.extra,
#game-replay-watch td.title,
#game-replay-watch td.users,
#game-replay-watch td.extra {
overflow-x: hidden;
white-space: nowrap;
}
#game-list td.users {
max-width: 95px;
}
#game-list td.extra {
max-width: 185px;
}
#game-replay-watch td.title {
max-width: 360px;
}
#game-replay-watch td.users {
max-width: 90px;
}
#game-replay-watch td.extra {
max-width: 120px;
}
#game-list td.extra span,
#game-replay-watch td.extra span {
margin-left: -2px;
padding-right: 2px;
border-right: 1px solid #999;
}
#game-list td.extra span:first-child,
#game-replay-watch td.extra span:first-child {
margin-left: 0;
}
#game-list td.extra span:last-child,
#game-replay-watch td.extra span:last-child {
padding-right: 0;
border-right: none;
}
#game-replay-watch .avatar {
width: 18px;
height: 18px;
}
#watch-filter {
display: inline-block;
}
#watch-filter .dropdown-item {
font-size: .875rem;
}
#watch-filter .form-check-input {
margin-left: inherit;
}
#join-private {
position: absolute;
left: .75rem;
bottom: .75rem;
width: 500px;
}
<div *ngIf="!matching" id="action">
<button [disabled]="!appsService.allReady(app) || currentServer.id !== 'tiramisu'" (click)="request_match('athletic')" type="button" class="btn btn-primary btn-sm">
<i class="fa fa-play" aria-hidden="true"></i> <span i18n>竞技匹配</span></button>
<button i18n [disabled]="!appsService.allReady(app) || currentServer.id !== 'tiramisu'" (click)="request_match('entertain')" type="button" class="btn btn-secondary btn-sm">娱乐匹配</button>
<button i18n [disabled]="!appsService.allReady(app) || !currentServer.custom" type="button" class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#game-list-modal">自定义游戏</button>
<button i18n [disabled]="!appsService.allReady(app) || !currentServer.windbot" type="button" class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#game-create-windbot">单人模式</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#game-replay-modal">观战录像</button>
</div>
<!-- 匹配中 -->
<div *ngIf="matching" id="match-time" class="input-group input-group-sm">
<span class="input-group-addon">
<i class="fa fa-futbol-o fa-spin" aria-hidden="true"></i>
<span i18n *ngIf="matching_arena == 'athletic'">竞技匹配</span>
<span i18n *ngIf="matching_arena == 'entertain'">娱乐匹配</span>
</span>
<span class="input-group-addon"><span i18n>预计时间</span> 03:00</span><span class="input-group-addon"><span i18n>实际时间</span> {{match_time}}</span>
<span class="input-group-btn"><button i18n class="btn btn-secondary" type="button" [disabled]="!match_cancelable" (click)="cancel_match()">取消</button></span>
</div>
<div class="row small-gutters">
<div class="col-sm-4 input-group input-group-sm">
<label class="input-group-text" id="server-label">环境</label>
<select class="form-select form-select-sm" id="selectServer" name="server" [disabled]="!appsService.allReady(app)" [(ngModel)]="currentServer">
<option *ngFor="let server of selectableServers" [value]="server">{{server.name}}</option>
</select>
</div>
<div class="col-sm-8 input-group input-group-sm">
<label i18n class="input-group-text" id="basic-addon1">卡组</label>
<select class="form-select form-select-sm" id="exampleSelect1" name="deck" [(ngModel)]="current_deck">
<optgroup *ngFor="let group of deckGroup()" [label]="group[0]">
<option *ngFor="let deck of group[1]" [value]="group[0] + '/' + deck">{{deck}}</option>
</optgroup>
</select>
<span class="input-group-btn">
<button id="edit_deck_button" i18n [disabled]="!appsService.allReady(app)" class="btn btn-secondary btn-sm" (click)="edit_deck(current_deck)">编辑</button>
</span>
</div>
</div>
<div class="modal fade" id="game-create-windbot" *ngIf="currentServer.windbot" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 i18n class="modal-title">单人模式</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<label i18n>选择对手</label>
<div id="windbot" class="list-group">
<a i18n href="#" class="list-group-item" (click)="join_windbot()">随机</a>
<a *ngFor="let name of windbot" href="#" class="list-group-item" (click)="join_windbot(name)">{{name}}</a>
</div>
</div>
<div class="modal-footer">
<button i18n type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="game-list-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content flex-row">
<div id="game-list">
<div i18n class="p-2" *ngIf="rooms_loading">正在读取游戏列表...</div>
<div i18n class="p-2" *ngIf="!rooms_loading && this.rooms.length === 0">现在没有等待中的游戏,可以自行创建一个房间或者去匹配</div>
<table *ngIf="!this.rooms_loading && this.rooms.length > 0" class="table table-striped table-hover">
<thead>
<tr>
<th i18n class="title">游戏标题</th>
<th i18n class="users">玩家</th>
<th i18n class="mode">决斗模式</th>
<th i18n class="extra">额外选项</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let room of rooms_show" class="room" (click)="join_room(room)">
<td class="title">{{room.title}}</td>
<td class="users">
<img *ngFor="let user of room.users" class="avatar rounded" [src]="'https://ygobbs.com/user_avatar/ygobbs.com/' + user.username + '/25/1.png'" data-bs-toggle="tooltip" data-placement="bottom" [title]="user.username" (error)="avatar_fallback($event)">
</td>
<td class="mode">
<span i18n *ngIf="room.options.mode === 0">单局模式</span>
<span i18n *ngIf="room.options.mode === 1">比赛模式</span>
<span i18n *ngIf="room.options.mode === 2">TAG</span>
</td>
<td class="extra">
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 0" title="允许OCG独有卡,不允许TCG独有卡">OCG</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 1" title="允许TCG独有卡,不允许OCG独有卡">TCG</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 2" title="只允许简体中文版已经发售的卡">简中</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 3" title="只允许自制卡">自制卡</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 4" title="不允许OCG或TCG独有卡">专有卡禁止</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 5" title="可以任意使用OCG或TCG卡">所有卡片</span>
<span i18n *ngIf="room.options.start_lp != default_options.start_lp">{{room.options.start_lp}}LP</span>
<span i18n i18n-title *ngIf="room.options.start_hand != default_options.start_hand" title="初始起手数量">{{room.options.start_hand}}初始</span>
<span i18n i18n-title *ngIf="room.options.draw_count != default_options.draw_count" title="每回合抽卡数量">{{room.options.draw_count}}抽卡</span>
<span i18n i18n-title *ngIf="room.options.duel_rule != default_options.duel_rule" title="上个版本的大师规则">大师规则{{room.options.duel_rule}}</span>
<span i18n i18n-title *ngIf="room.options.no_check_deck != default_options.no_check_deck" title="不检查卡组是否合规">不检查</span>
<span i18n i18n-title *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck" title="任何时候都不洗切卡组">不洗卡</span>
<span i18n i18n-title *ngIf="!!room.options.auto_death" title="40分钟自动死三">自动加时赛</span>
</td>
</tr>
</tbody>
</table>
<div id="join-private" class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-key"></i></span>
<input [(ngModel)]="join_password" type="text" class="form-control" i18n-placeholder placeholder="在这输入你朋友的私密房间密码就可以进去了哦!">
<button i18n class="btn btn-secondary" type="button" (click)="join_private(join_password)">加入私密房间</button>
</div>
</div>
<form (submit)="create_room(room)">
<h3 i18n>创建房间</h3>
<div class="form-group">
<ng-container *ngIf="!room.private; else private">
<label i18n for="game-create-title">游戏标题</label>
<input type="text" maxlength="12" class="form-control form-control-sm" id="game-create-title" name="title" [(ngModel)]="room.title" required [readonly]>
<small i18n class="form-text text-muted">最多 12 个字</small>
</ng-container>
<ng-template #private>
<label *ngIf="room.private" for="game-create-title"><i class="fa fa-key" aria-hidden="true"></i>
<span i18n>房间密码</span></label>
<div class="input-group input-group-sm">
<input type="text" maxlength="12" class="form-control" id="game-create-title" name="title" [(ngModel)]="host_password" readonly>
<span i18n-title id="copy-wrapper" class="input-group-btn" data-bs-toggle="tooltip" title="房间密码已复制到剪贴板">
<button i18n-title class="btn btn-secondary fa fa-clipboard" type="button" title="复制" (click)="copy(host_password, $event)"></button>
</span>
</div>
<small i18n class="form-text text-muted">把这个分享给你的朋友</small>
</ng-template>
</div>
<div class="form-group">
<label i18n for="game-create-rule">卡片允许</label>
<select class="form-control form-control-sm" id="game-create-rule" name="rule" [(ngModel)]="room.options.rule">
<option i18n value="0">OCG</option>
<option i18n value="1">TCG</option>
<option i18n value="2">简体中文</option>
<option i18n value="3">自制卡</option>
<option i18n value="4">专有卡禁止</option>
<option i18n value="5">所有卡片</option>
</select>
</div>
<div class="form-group">
<label i18n for="game-create-mode">决斗模式</label>
<select class="form-control form-control-sm" id="game-create-mode" name="mode" (change)="room.options.start_lp = room.options.mode == 2 ? default_options.start_lp_tag : default_options.start_lp" [(ngModel)]="room.options.mode">
<option i18n value="0">单局模式</option>
<option i18n value="1">比赛模式</option>
<option i18n value="2">TAG</option>
</select>
</div>
<div class="form-group">
<label i18n for="game-create-duelrule">决斗规则</label>
<select class="form-control form-control-sm" id="game-create-duelrule" name="duel_rule" [(ngModel)]="room.options.duel_rule">
<option i18n value="1">大师规则1</option>
<option i18n value="2">大师规则2</option>
<option i18n value="3">大师规则3</option>
<option i18n value="4">新大师规则</option>
<option i18n value="5">大师规则2020</option>
</select>
</div>
<fieldset>
<legend i18n class="col-form-legend">额外选项</legend>
<div class="row">
<label i18n for="game-create-start-lp" class="col-sm-6 col-form-label">初始 LP</label>
<div class="col-sm-6">
<input type="number" value="8000" min="1" max="65536" class="form-control form-control-sm" id="game-create-start-lp" name="start_lp" [(ngModel)]="room.options.start_lp">
</div>
</div>
<div class="row">
<label i18n for="game-create-start-hand" class="col-sm-6 col-form-label">初始手牌数</label>
<div class="col-sm-6">
<input type="number" value="5" min="0" max="16" class="form-control form-control-sm" id="game-create-start-hand" name="start_hand" [(ngModel)]="room.options.start_hand">
</div>
</div>
<div class="row">
<label i18n for="game-create-draw-count" class="col-sm-6 col-form-label">每回合抽卡</label>
<div class="col-sm-6">
<input type="number" value="1" min="0" max="16" class="form-control form-control-sm" id="game-create-draw-count" name="draw_count" [(ngModel)]="room.options.draw_count">
</div>
</div>
<div>
<input id="private" name="private" [(ngModel)]="room.private" type="checkbox">
<label i18n for="private">私密房间</label>
</div>
<!--div>
<input id="enable_priority" name="enable_priority" [(ngModel)]="room.options.enable_priority" type="checkbox">
<label i18n for="enable_priority">旧规则</label>
</div-->
<div>
<input id="no_check_deck" name="no_check_deck" [(ngModel)]="room.options.no_check_deck" type="checkbox">
<label i18n for="no_check_deck">不检查卡组</label>
</div>
<div>
<input id="no_shuffle_deck" name="no_shuffle_deck" type="checkbox" [(ngModel)]="room.options.no_shuffle_deck">
<label i18n for="no_shuffle_deck">不洗切卡组</label>
</div>
<div>
<input id="auto_death" name="auto_death" type="checkbox" [(ngModel)]="room.options.auto_death">
<label i18n for="auto_death">40分自动加时赛</label>
</div>
</fieldset>
<div id="game-create-actions">
<button i18n type="submit" class="btn btn-sm btn-primary">创建房间</button>
</div>
</form>
<button id="game-list-close" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
<div class="modal fade" id="game-replay-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<!--<div class="modal-header">-->
<!--<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>-->
<!--<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">-->
<!--<span aria-hidden="true">&times;</span>-->
<!--</button>-->
<!--</div>-->
<div class="modal-body">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<ul class="nav nav-tabs" role="tablist">
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-bs-toggle="tab" href="#home" role="tab">推荐</a>-->
<!--</li>-->
<li class="nav-item">
<a i18n class="nav-link active" data-bs-toggle="tab" href="#game-replay-watch" role="tab">观战</a>
</li>
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-bs-toggle="tab" href="#home" role="tab">收藏的录像</a>-->
<!--</li>-->
<li class="nav-item">
<a i18n class="nav-link" data-bs-toggle="tab" href="#game-replay-local" role="tab">本地录像</a>
</li>
<!--<li *ngIf="settingsService.getLocale().startsWith('zh')" class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#game-replay-bilibili" role="tab">哔哩哔哩</a>
</li>-->
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-bs-toggle="tab" href="#game-replay-youtube" role="tab">YouTube</a>-->
<!--</li>-->
</ul>
<div class="tab-content">
<div class="tab-pane active scroll" id="game-replay-watch" role="tabpanel">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="mode">
<!--<span i18n>游戏模式</span>-->
<div id="watch-filter" class="dropdown">
<button i18n class="btn btn-secondary dropdown-toggle btn-sm" type="button" id="watchDropdownMenuButton" aria-haspopup="true" aria-expanded="false">游戏模式</button>
<div class="dropdown-menu">
<h6 i18n class="dropdown-header">匹配</h6>
<label i18n class="form-check dropdown-item">
<input type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.athletic">
竞技匹配
</label>
<label i18n class="form-check dropdown-item">
<input type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.entertain">
娱乐匹配
</label>
<h6 i18n class="dropdown-header">自定义游戏</h6>
<label i18n class="form-check dropdown-item">
<input type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.single">
单局模式
</label>
<label i18n class="form-check dropdown-item">
<input type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.match">
比赛模式
</label>
<label i18n class="form-check dropdown-item">
<input type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.tag">
TAG
</label>
<h6 i18n class="dropdown-header">单人模式</h6>
<label i18n class="form-check dropdown-item">
<input type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.windbot">
单人模式
</label>
</div>
</div>
</th>
<th i18n class="title">游戏标题</th>
<th i18n class="users">玩家</th>
<th i18n class="extra">额外选项</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let room of replay_rooms_show" class="room" (click)="join_room(room)">
<td class="mode">
<span i18n *ngIf="room.id!.startsWith('AI#')">单人模式</span>
<span i18n *ngIf="room.arena === 'athletic'">竞技匹配</span>
<span i18n *ngIf="room.arena === 'entertain'">娱乐匹配</span>
<span i18n *ngIf="!(room.arena || room.id!.startsWith('AI#')) && room.options.mode === 0">单局模式</span>
<span i18n *ngIf="!(room.arena || room.id!.startsWith('AI#')) && room.options.mode === 1">比赛模式</span>
<span i18n *ngIf="!(room.arena || room.id!.startsWith('AI#')) && room.options.mode === 2">TAG</span>
</td>
<td class="title">
<span i18n *ngIf="room.private">{{room.users![0] && room.users![0].username}} 的私密房间</span>
<span i18n *ngIf="room.arena || room.id!.startsWith('AI#')">{{room.users![0] && room.users![0].username}} 跟 {{room.users![1] && room.users![1].username}} 的决斗</span>
<span *ngIf="!(room.arena || room.id!.startsWith('AI#') || room.private)">{{room.title}}</span>
</td>
<td class="users">
<img *ngFor="let user of room.users" class="avatar rounded" [src]="'https://ygobbs.com/user_avatar/ygobbs.com/' + user.username + '/25/1.png'" data-bs-toggle="tooltip" data-placement="bottom" [title]="user.username" (error)="avatar_fallback($event)">
</td>
<td class="extra">
<div *ngIf="!(room.arena || room.id!.startsWith('AI#'))">
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 0" title="允许OCG独有卡,不允许TCG独有卡">OCG</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 1" title="允许TCG独有卡,不允许OCG独有卡">TCG</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 2" title="只允许简体中文版已经发售的卡">简中</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 3" title="只允许自制卡">自制卡</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 4" title="不允许OCG或TCG独有卡">专有卡禁止</span>
<span i18n i18n-title *ngIf="room.options.rule != default_options.rule && room.options.rule == 5" title="可以任意使用OCG或TCG卡">所有卡片</span>
<span i18n *ngIf="room.options.start_lp != default_options.start_lp && room.options.mode != 2">{{room.options.start_lp}}LP</span>
<span i18n *ngIf="room.options.start_lp != default_options.start_lp_tag && room.options.mode == 2">{{room.options.start_lp}}LP</span>
<span i18n i18n-title *ngIf="room.options.start_hand != default_options.start_hand" title="初始起手数量">{{room.options.start_hand}}初始</span>
<span i18n i18n-title *ngIf="room.options.draw_count != default_options.draw_count" title="每回合抽卡数量">{{room.options.draw_count}}抽卡</span>
<span i18n i18n-title *ngIf="room.options.duel_rule != default_options.duel_rule" title="上个版本的大师规则">大师规则{{room.options.duel_rule}}</span>
<span i18n i18n-title *ngIf="room.options.no_check_deck != default_options.no_check_deck" title="不检查卡组是否合规">不检查</span>
<span i18n i18n-title *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck" title="任何时候都不洗切卡组">不洗卡</span>
<span i18n i18n-title *ngIf="!!room.options.auto_death" title="40分钟自动死三">自动加时赛</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane" id="game-replay-local" role="tabpanel">
<div class="list-group">
<a *ngFor="let replay of replays" class="list-group-item list-group-item-action" (click)="watch_replay(replay)">{{replay}}</a>
</div>
</div>
<!--<div *ngIf="settingsService.getLocale().startsWith('zh')" class="tab-pane" id="game-replay-bilibili" role="tabpanel">
<webview #bilibili src="http://m.bilibili.com/search.html?keyword=YGOPro" (did-finish-load)="bilibili_loaded()" (will-navigate)="bilibili_navigate($event)"></webview>
</div>-->
<!--<div class="tab-pane" id="game-replay-youtube" role="tabpanel">-->
<!--<webview #youtube src="https://m.youtube.com/results?search_query=YGOPro" (did-finish-load)="youtube_loaded()" (will-navigate)="youtube_navigate($event)"></webview>-->
<!--</div>-->
</div>
</div>
<!--<div class="modal-footer">-->
<!--<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>-->
<!--&lt;!&ndash;<button type="button" class="btn btn-primary">Save changes</button>&ndash;&gt;-->
<!--</div>-->
</div>
</div>
</div>
/**
* Created by zh99998 on 16/9/2.
*/
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { clipboard, shell } from 'electron';
import * as remote from '@electron/remote'
import fs from 'fs-extra';
import $ from 'jquery';
import path from 'path';
import { App } from '../shared/app';
import { AppsService } from '../apps.service';
import { LoginService } from '../login/login.service';
import { SettingsService } from '../settings.service';
import _ from 'lodash';
import fg from 'fast-glob';
import { HttpClient } from '@angular/common/http';
import WillNavigateEvent = Electron.WillNavigateEvent;
import Timer = NodeJS.Timer;
import { Subscription } from 'rxjs';
interface SystemConf {
use_d3d: string;
antialias: string;
errorlog: string;
nickname: string;
gamename: string;
lastdeck: string;
textfont: string;
numfont: string;
serverport: string;
lastip: string;
lasthost: string;
lastport: string;
autopos: string;
randompos: string;
autochain: string;
waitchain: string;
mute_opponent: string;
mute_spectators: string;
hide_setname: string;
hide_hint_button: string;
control_mode: string;
draw_field_spell: string;
separate_clear_button: string;
roompass: string;
}
interface Server {
id: string;
name?: string;
url?: string;
address: string;
port: number;
hidden?: boolean;
custom?: boolean;
replay?: boolean;
windbot?: string[];
}
interface Room {
id?: string;
title?: string;
server?: Server;
'private'?: boolean;
options: Options;
arena?: string;
users?: { username: string, position: number }[];
}
interface Options {
mode: number;
rule: number;
start_lp: number;
start_lp_tag: number;
start_hand: number;
draw_count: number;
duel_rule: number;
no_check_deck: boolean;
no_shuffle_deck: boolean;
lflist?: number;
time_limit?: number;
auto_death: boolean;
}
// export interface Points {
// exp: number;
// exp_rank: number;
// pt: number;
// arena_rank: number;
// win: number;
// lose: number;
// draw: number;
// all: number;
// ratio: number;
// }
interface YGOProDistroData {
deckPath: string;
replayPath: string;
systemConf?: string;
lastDeckFormat?: string;
}
interface YGOProData {
ygopro: YGOProDistroData;
servers: Server[];
}
let matching: Subscription | undefined;
let matching_arena: string | undefined;
let match_started_at: Date;
@Component({
moduleId: module.id,
selector: 'ygopro',
templateUrl: 'ygopro.component.html',
styleUrls: ['ygopro.component.css']
})
export class YGOProComponent implements OnInit, OnDestroy {
@Input()
app: App;
@Input()
currentApp: App;
@Output()
points: EventEmitter<any> = new EventEmitter();
decks: string[] = [];
decks_grouped: [string, string[]][];
replays: string[] = [];
current_deck: string;
system_conf?: string;
numfont: string[];
textfont: string[];
@ViewChild('bilibili')
bilibili: ElementRef;
@ViewChild('youtube')
youtube: ElementRef;
// points: Points;
servers: Server[];
selectableServers: Server[];
// selectingServerId: string;
currentServer: Server;
// tslint:disable-next-line:member-ordering
rooms_loading = true;
/*reloadCurrentServer() {
this.currentServer = this.servers.find(s => s.id === this.selectingServerId);
}*/
lastDeckFormat: RegExp;
default_options: Options = {
mode: 1,
rule: this.settingsService.getLocale().startsWith('zh') ? 0 : 1,
start_lp: 8000,
start_lp_tag: 16000,
start_hand: 5,
draw_count: 1,
duel_rule: 5,
no_check_deck: false,
no_shuffle_deck: false,
lflist: 0,
time_limit: 180,
auto_death: false
};
room: Room = { title: this.loginService.user.username + '的房间', options: Object.assign({}, this.default_options) };
rooms: Room[] = [];
rooms_show: Room[];
connections: WebSocket[] = [];
replay_connections: WebSocket[] = [];
replay_rooms: Room[] = [];
replay_rooms_show: Room[];
replay_rooms_filter = {
athletic: true,
entertain: true,
single: true,
match: true,
tag: true,
windbot: false
};
matching: Subscription | undefined;
matching_arena: string | undefined;
match_time: string;
match_cancelable: boolean;
match_interval: Timer | undefined;
join_password: string;
host_password = (this.loginService.user.external_id ^ 0x54321).toString();
constructor(private http: HttpClient, public appsService: AppsService, private loginService: LoginService,
public settingsService: SettingsService, private ref: ChangeDetectorRef) {
switch (process.platform) {
// linux should have fonts set by default
case 'linux':
this.numfont = [
'/usr/share/fonts/truetype/DroidSansFallbackFull.ttf',
'/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Bold.ttc',
'/usr/share/fonts/noto-cjk/NotoSansCJK-Bold.ttc'
];
this.textfont = [
'/usr/share/fonts/truetype/DroidSansFallbackFull.ttf',
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc'
];
break;
case 'darwin':
this.numfont = ['/System/Library/Fonts/SFNSTextCondensed-Bold.otf', '/System/Library/Fonts/Supplemental/Arial.ttf'];
this.textfont = ['/System/Library/Fonts/PingFang.ttc'];
break;
case 'win32':
this.numfont = [path.join(process.env['SystemRoot']!, 'Fonts', 'arialbd.ttf')];
this.textfont = [
path.join(process.env['SystemRoot']!, 'Fonts', 'msyh.ttc'),
path.join(process.env['SystemRoot']!, 'Fonts', 'msyh.ttf'),
path.join(process.env['SystemRoot']!, 'Fonts', 'simsun.ttc')
];
break;
}
if (matching) {
this.matching = matching;
this.matching_arena = matching_arena;
this.refresh_match();
this.match_interval = setInterval(() => {
this.refresh_match();
}, 1000);
}
}
get windbot() {
return this.currentServer.windbot;
}
refresh_rooms() {
this.rooms_show = this.rooms.filter((room) => room.server === this.currentServer);
}
refresh_replay_rooms() {
this.replay_rooms_show = this.replay_rooms.filter((room) => {
if (!room.arena && room.server && room.server !== this.currentServer) {
return false;
}
return ((this.replay_rooms_filter.athletic && room.arena === 'athletic') ||
(this.replay_rooms_filter.entertain && room.arena === 'entertain') ||
(this.replay_rooms_filter.single && room.options.mode === 0 && !room.arena && !room.id!.startsWith('AI#')) ||
(this.replay_rooms_filter.match && room.options.mode === 1 && !room.arena && !room.id!.startsWith('AI#')) ||
(this.replay_rooms_filter.tag && room.options.mode === 2 && !room.arena && !room.id!.startsWith('AI#')) ||
(this.replay_rooms_filter.windbot && room.id!.startsWith('AI#')));
}).sort((a, b) => {
// if (a.arena === 'athletic' && b.arena === 'athletic') {
// return a.dp - b.dp;
// } else if (a.arena === 'entertain' && b.arena === 'entertain') {
// return a.exp - b.exp;
// }
let [a_priority, b_priority] = [a, b].map((room) => {
if (room.arena === 'athletic') {
return 0;
} else if (room.arena === 'entertain') {
return 1;
} else if (room.id!.startsWith('AI#')) {
return 5;
} else {
return room.options.mode + 2;
}
});
return a_priority - b_priority;
});
}
getYGOProData(app: App) {
const ygoproData = <YGOProData>app.data;
for (const child of this.appsService.findChildren(app)) {
if (child.isYGOPro && child.isInstalled() && child.isReady()) {
const childData = this.getYGOProData(child);
_.mergeWith(ygoproData, childData, (objValue, srcValue) => {
if (_.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
}
}
return ygoproData;
}
async ngOnInit() {
const ygoproData = this.getYGOProData(this.app);
this.servers = ygoproData.servers;
this.selectableServers = this.servers.filter(s => !s.hidden);
this.currentServer = this.selectableServers[0];
// this.reloadCurrentServer();
let locale: string;
if (this.settingsService.getLocale().startsWith('zh')) {
locale = 'zh-CN';
} else {
locale = 'en-US';
}
if (ygoproData.ygopro.lastDeckFormat) {
// console.log(`Deck format pattern: ${ygoproData.ygopro.lastDeckFormat}`)
this.lastDeckFormat = new RegExp(ygoproData.ygopro.lastDeckFormat);
}
this.system_conf = this.app.systemConfPath;
console.log(`Will load system conf file from ${this.system_conf}`);
await this.refresh(true);
let modal = $('#game-list-modal');
modal.on('show.bs.modal', () => {
this.rooms_loading = true;
this.connections = this.servers.filter(server => server.custom).map((server) => {
let url = new URL(server.url!);
url['searchParams'].set('filter', 'waiting');
let connection = new WebSocket(url.toString());
connection.onclose = (event: CloseEvent) => {
this.rooms = this.rooms.filter(room => room.server !== server);
this.refresh_rooms();
};
connection.onerror = (event) => {
console.log('error', server.id, event);
this.rooms = this.rooms.filter(room => room.server !== server);
this.refresh_rooms();
};
connection.onmessage = (event) => {
let message = JSON.parse(event.data);
switch (message.event) {
case 'init':
this.rooms_loading = false;
this.rooms = this.rooms.filter(room => room.server !== server).concat(
message.data.map((room: Room) => Object.assign({ server: server }, room))
);
break;
case 'create':
this.rooms.push(Object.assign({ server: server }, message.data));
break;
case 'update':
Object.assign(this.rooms.find(room => room.server === server && room.id === message.data.id), message.data);
break;
case 'delete':
this.rooms.splice(this.rooms.findIndex(room => room.server === server && room.id === message.data), 1);
}
this.refresh_rooms();
this.ref.detectChanges();
};
return connection;
});
});
modal.on('hide.bs.modal', () => {
for (let connection of this.connections) {
connection.close();
}
this.connections = [];
});
// TODO: 跟上面的逻辑合并
let replay_modal = $('#game-replay-modal');
replay_modal.on('show.bs.modal', () => {
this.replay_connections = this.servers.filter(server => server.replay).map((server) => {
let url = new URL(server.url!);
url['searchParams'].set('filter', 'started');
let connection = new WebSocket(url.toString());
connection.onclose = () => {
this.replay_rooms = this.replay_rooms.filter(room => room.server !== server);
this.refresh_replay_rooms();
};
connection.onmessage = (event) => {
let message = JSON.parse(event.data);
switch (message.event) {
case 'init':
this.replay_rooms = this.replay_rooms.filter(room => room.server !== server).concat(
message.data.map((room: Room) => Object.assign({
server: server,
'private': /^\d+$/.test(room.title!)
}, room))
);
break;
case 'create':
this.replay_rooms.push(Object.assign({
server: server,
'private': /^\d+$/.test(message.data.title!)
}, message.data));
break;
case 'delete':
this.replay_rooms.splice(
this.replay_rooms.findIndex(room => room.server === server && room.id === message.data),
1
);
}
this.refresh_replay_rooms();
this.ref.detectChanges();
};
return connection;
});
});
replay_modal.on('hide.bs.modal', () => {
for (let connection of this.replay_connections) {
connection.close();
}
this.replay_connections = [];
});
let watchDropdownMenu = $('#watch-filter');
watchDropdownMenu.on('change', 'input[type=\'checkbox\']', (event) => {
// $(event.target).closest("label").toggleClass("active", (<HTMLInputElement> event.target).checked);
this.refresh_replay_rooms();
});
replay_modal.on('click', (event) => {
if (!watchDropdownMenu.is(event.target) && !watchDropdownMenu.has(event.target).length) {
watchDropdownMenu.removeClass('show');
}
});
$('#watchDropdownMenuButton').on('click', () => {
watchDropdownMenu.toggleClass('show');
});
remote.ipcMain.on('YGOPro', (e: any, type: string) => {
console.log('rrrrr');
this.request_match(type);
});
}
async refresh(init?: boolean) {
this.decks = await this.get_decks();
this.decks_grouped = this.deckGroup();
if (this.lastDeckFormat) {
const systemConfString = await this.load_system_conf();
let lastDeck: string | undefined = undefined;
if (systemConfString) {
// console.log(`System conf string: ${systemConfString}`);
const lastDeckMatch = systemConfString.match(this.lastDeckFormat);
if (lastDeckMatch) {
lastDeck = lastDeckMatch[1];
// console.log(`Last deck ${lastDeck} read from ${this.system_conf}.`);
} else {
// console.error(`Deck pattern not found from pattern ${this.system_conf}: ${lastDeckMatch}`);
}
} else {
// console.error(`System conf ${this.system_conf} not found.`);
}
if (lastDeck && this.decks.includes(lastDeck)) {
// console.log(`Got last deck ${lastDeck}.`);
this.current_deck = lastDeck;
} else if (init) {
this.current_deck = this.decks[0];
}
}
this.replays = await this.get_replays();
// https://mycard.moe/ygopro/api/user?username=ozxdno
try {
let points = await this.http.get<any>('https://mycard.moe/ygopro/api/user', {
params: {
username: this.loginService.user.username
}
})
.toPromise();
this.points.emit(points);
} catch (error) {
console.log(error);
}
};
async get_decks(): Promise<string[]> {
try {
return fg.sync('**/*.ydk', { cwd: this.app.ygoproDeckPath });
} catch (error) {
console.error(`Load deck fail: ${error.toString()}`);
return [];
}
}
deckGroup(): [string, string[]][] {
return Object.entries(_.mapValues(_.groupBy(this.decks, p => path.dirname(p)), g => g.map(p => path.basename(p, '.ydk'))));
}
async get_replays(): Promise<string[]> {
try {
let files: string[] = await fs.readdir(this.app.ygoproReplayPath!);
return files.filter(file => path.extname(file) === '.yrp').map(file => path.basename(file, '.yrp'));
} catch (error) {
console.error(`Load replay fail: ${error.toString()}`);
return [];
}
}
async get_font(files: string[]): Promise<string | undefined> {
for (let file of files) {
if (await fs.pathExists(file)) {
return file;
}
}
return;
}
async delete_deck(deck: string) {
if (confirm('确认删除?')) {
try {
await fs.unlink(path.join(this.app.ygoproDeckPath!, deck + '.ydk'));
} catch (error) {
}
return this.refresh();
}
}
/*
async fix_fonts(data: SystemConf) {
if (!await this.get_font([data.numfont])) {
let font = await this.get_font(this.numfont);
if (font) {
data['numfont'] = font;
}
}
if (data.textfont === 'c:/windows/fonts/simsun.ttc 14' || !await this.get_font([data.textfont.split(' ', 2)[0]])) {
let font = await this.get_font(this.textfont);
if (font) {
data['textfont'] = `${font} 14`;
}
}
};*/
async load_system_conf(): Promise<string | undefined> {
if (!this.system_conf) {
return;
}
try {
// console.log(`Loading system conf from ${this.system_conf}`)
let data = await fs.readFile(this.system_conf, { encoding: 'utf-8' });
return data;
} catch (e) {
return;
}
};
/*
save_system_conf(data: SystemConf) {
return fs.writeFile(this.system_conf, ini.unsafe(ini.stringify(data, <EncodeOptions>{whitespace: true})));
};*/
async join(name: string, server: Server) {
/*let system_conf = await this.load_system_conf();
await this.fix_fonts(system_conf);
system_conf.lastdeck = this.current_deck;
system_conf.lastip = server.address;
system_conf.lasthost = server.address;
system_conf.lastport = server.port.toString();
system_conf.roompass = name;
system_conf.nickname = this.loginService.user.username;
await this.save_system_conf(system_conf);*/
// return this.start_game(['-h', server.address, '-p', server.port.toString(), '-w', name, '-n', this.loginService.user.username, '-d', this.current_deck, '-j']);
return this.start_game('main', { server, password: name, username: this.loginService.user.username, deck: this.current_deck });
};
async edit_deck(deck: string) {
/*let system_conf = await this.load_system_conf();
await this.fix_fonts(system_conf);
system_conf.lastdeck = deck;
await this.save_system_conf(system_conf);*/
// return this.start_game(['-d', deck]);
return this.start_game('deck', { deck });
}
async watch_replay(replay: string) {
/*let system_conf = await this.load_system_conf();
await this.fix_fonts(system_conf);
await this.save_system_conf(system_conf);*/
// return this.start_game(['-r', path.join('replay', replay + '.yrp')]);
return this.start_game('replay', { replay: path.join(this.app.ygoproReplayPath!, `${replay}.yrp`) });
}
join_windbot(name?: string) {
if (!name) {
name = this.windbot![Math.floor(Math.random() * this.windbot!.length)];
}
return this.join('AI#' + name, this.currentServer);
}
async start_game(action: string, param: any) {
let data: any;
let start_time: string;
let exp_rank_ex: number;
let arena_rank_ex: number;
let win = remote.getCurrentWindow();
win.minimize();
await new Promise(async (resolve, reject) => {
const children = this.appsService.findChildren(this.app);
let child = await this.app.spawnApp(children, action, param);
child.on('error', (error) => {
reject(error);
win.restore();
});
child.on('exit', async (code, signal) => {
// error 触发之后还可能会触发exit,但是Promise只承认首次状态转移,因此这里无需重复判断是否已经error过。
await this.refresh();
resolve(null);
win.restore();
});
try {
this.http.get<any>('https://mycard.moe/ygopro/api/history', {
params: {
page: 1,
username: this.loginService.user.username,
type: 0,
page_num: 1
}
})
.toPromise()
.then((d) => {
start_time = d.data[0].start_time;
});
} catch (error) {
console.log(error);
}
try {
this.http.get<any>('https://sapi.moecube.com:444/ygopro/arena/user', { params: { username: this.loginService.user.username } })
.toPromise()
.then((d2) => {
exp_rank_ex = d2.exp_rank;
arena_rank_ex = d2.arena_rank;
});
} catch (error) {
console.log(error);
}
});
try {
await this.http.get<any>('https://mycard.moe/ygopro/api/history', {
params: {
page: 1,
username: this.loginService.user.username,
// username: "星光pokeboy",
type: 0,
page_num: 1
}
})
.toPromise()
.then((d) => {
data = d.data[0];
data.myname = this.loginService.user.username;
});
await this.http.get<any>('https://sapi.moecube.com:444/ygopro/arena/user', {
params: {
username: this.loginService.user.username
}
})
.toPromise()
.then((data2) => {
data.athletic_win = data2.athletic_win;
data.athletic_lose = data2.athletic_lose;
data.entertain_win = data2.entertain_win;
data.entertain_lose = data2.entertain_lose;
data.exp_rank = data2.exp_rank;
data.arena_rank = data2.arena_rank;
data.exp_rank_ex = exp_rank_ex;
data.arena_rank_ex = arena_rank_ex;
if (start_time !== data.start_time) {
this.appsService.showResult('app/end_YGOPro_single.html', data, 202, 222);
}
});
} catch (error) {
console.log(error);
}
};
create_room(room: Room) {
let options_buffer = Buffer.alloc(6);
// 建主密码 https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit
options_buffer.writeUInt8(((room.private ? 2 : 1) << 4) |
(room.options.duel_rule << 1) |
(room.options.auto_death ? 0x1 : 0), 1);
options_buffer.writeUInt8(
room.options.rule << 5 |
room.options.mode << 3 |
(room.options.no_check_deck ? 1 << 1 : 0) |
(room.options.no_shuffle_deck ? 1 : 0)
, 2);
options_buffer.writeUInt16LE(room.options.start_lp, 3);
options_buffer.writeUInt8(room.options.start_hand << 4 | room.options.draw_count, 5);
let checksum = 0;
for (let i = 1; i < options_buffer.length; i++) {
checksum -= options_buffer.readUInt8(i);
}
options_buffer.writeUInt8(checksum & 0xFF, 0);
let secret = this.loginService.user.external_id % 65535 + 1;
for (let i = 0; i < options_buffer.length; i += 2) {
options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i);
}
let password = options_buffer.toString('base64') + (room.private ? this.host_password :
room.title!.replace(/\s/, String.fromCharCode(0xFEFF)));
// let room_id = crypto.createHash('md5').update(password + this.loginService.user.username).digest('base64')
// .slice(0, 10).replace('+', '-').replace('/', '_');
if (room.private) {
new Notification('YGOPro 私密房间已建立', {
body: `房间密码是 ${this.host_password}, 您的对手可在自定义游戏界面输入密码与您对战。`
});
}
this.join(password, this.currentServer);
}
copy(text: string, event: Event) {
clipboard.writeText(text);
// const copyWrapper = $('#copy-wrapper');
// copyWrapper.tooltip({ trigger: 'manual' });
// copyWrapper.tooltip('show');
}
join_room(room: Room) {
let options_buffer = Buffer.alloc(6);
options_buffer.writeUInt8(3 << 4, 1);
let checksum = 0;
for (let i = 1; i < options_buffer.length; i++) {
checksum -= options_buffer.readUInt8(i);
}
options_buffer.writeUInt8(checksum & 0xFF, 0);
let secret = this.loginService.user.external_id % 65535 + 1;
for (let i = 0; i < options_buffer.length; i += 2) {
options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i);
}
let name = options_buffer.toString('base64') + room.id;
this.join(name, room.server!);
}
join_private(password: string) {
let options_buffer = Buffer.alloc(6);
options_buffer.writeUInt8(5 << 4, 1);
let checksum = 0;
for (let i = 1; i < options_buffer.length; i++) {
checksum -= options_buffer.readUInt8(i);
}
options_buffer.writeUInt8(checksum & 0xFF, 0);
let secret = this.loginService.user.external_id % 65535 + 1;
for (let i = 0; i < options_buffer.length; i += 2) {
options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i);
}
let name = options_buffer.toString('base64') + password.replace(/\s/, String.fromCharCode(0xFEFF));
this.join(name, this.currentServer);
}
request_match(arena = 'entertain') {
match_started_at = new Date();
this.matching_arena = matching_arena = arena;
this.matching = matching = this.http.post<any>('https://sapi.moecube.com:444/ygopro/match', null, {
headers: {
Authorization: 'Basic ' + Buffer.from(this.loginService.user.username + ':' + this.loginService.user.external_id).toString('base64')
},
params: {
arena,
locale: this.settingsService.getLocale()
}
})
.subscribe((data) => {
this.join(data['password'], { id: '_match', address: data['address'], port: data['port'] });
}, (error) => {
alert(`匹配失败`);
this.matching = matching = undefined;
this.matching_arena = matching_arena = undefined;
if (this.match_interval) {
clearInterval(this.match_interval);
this.match_interval = undefined;
}
}, () => {
this.matching = matching = undefined;
this.matching_arena = matching_arena = undefined;
if (this.match_interval) {
clearInterval(this.match_interval);
this.match_interval = undefined;
}
});
this.refresh_match();
this.match_interval = setInterval(() => {
this.refresh_match();
}, 1000);
}
cancel_match() {
this.matching!.unsubscribe();
this.matching = matching = undefined;
this.matching_arena = matching_arena = undefined;
if (this.match_interval) {
clearInterval(this.match_interval);
this.match_interval = undefined;
}
}
ngOnDestroy() {
if (this.match_interval) {
clearInterval(this.match_interval);
this.match_interval = undefined;
}
remote.ipcMain.removeAllListeners('YGOPro');
}
refresh_match() {
let match_time = Math.floor((new Date().getTime() - match_started_at.getTime()) / 1000);
let minute = Math.floor(match_time / 60).toString();
if (minute.length === 1) {
minute = '0' + minute;
}
let second = (match_time % 60).toString();
if (second.length === 1) {
second = '0' + second;
}
this.match_time = `${minute}:${second}`;
this.match_cancelable = match_time <= 5 || match_time >= 180;
}
bilibili_loaded() {
this.bilibili.nativeElement.insertCSS(`
#b_app_link {
visibility: hidden;
}
.wrapper {
padding-top: 0 !important;
overflow-y: hidden;
}
.nav-bar, .top-title, .roll-bar, footer {
display: none !important;
}
html, body {
background-color: initial !important;
}
`);
}
bilibili_navigate(event: WillNavigateEvent) {
// event.preventDefault();
// https://github.com/electron/electron/issues/1378
this.bilibili.nativeElement.src = 'http://m.bilibili.com/search.html?keyword=YGOPro';
shell.openExternal(event.url);
}
// youtube_loaded () {
//
// }
//
// youtube_navigate (event: WillNavigateEvent) {
// this.youtube.nativeElement.src = 'https://m.youtube.com/results?search_query=YGOPro';
// shell.openExternal(event.url);
// }
avatar_fallback(event) {
if (!event.target.getAttribute('fallback')) {
event.target.src = 'assets/noavatar.png';
event.target.setAttribute('fallback', true);
}
}
}
This diff was suppressed by a .gitattributes entry.
<!doctype html>
<html lang="en">
<html lang='en'>
<head>
<meta charset="utf-8">
<meta charset='utf-8'>
<title>Mycard</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<base href='/'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='icon' type='image/x-icon' href='favicon.ico'>
</head>
<body>
<app-root></app-root>
<mycard>
<!--<div id="loading">MyCard <span id="version"></span> Loading...</div>-->
<div id='loading-bar'>
<span class='navbar-brand'>MyCard</span>
<i class='fa fa-times close' i18n='' i18n-title='' title='关闭' onclick='window.close()'></i>
</div>
<div id='loading'>
<!-- <img src='assets/icon.ico'>-->
<p>
LOADING
<span>.</span>
<span>.</span>
<span>.</span>
</p>
</div>
<div id='failed' hidden>发生了错误,请复制以下错误信息并联系 support@mycard.moe</div>
<pre id='error' hidden></pre>
</mycard>
<script>
document.body.classList.add(process.platform);
// document.getElementById('version').innerHTML = require('electron').remote.app.getVersion();
</script>
</body>
</html>
/* You can add global styles to this file, and also import other style files */
// First override some or all individual color variables
$primary: #00a4d9;
$secondary: #8f5325;
$success: #3e8d63;
$info: #13101c;
$warning: #945707;
$danger: #d62518;
$light: #f7f7f9;
$dark: #343a40;
// Then add them to your custom theme-colors map, together with any additional colors you might need
$theme-colors: (
primary: $primary,
secondary: $secondary,
success: $success,
info: $info,
warning: $warning,
danger: $danger,
light: $light,
dark: $dark,
// add any additional color below
);
//$primary: #00a4d9;
//$secondary: #8f5325;
//$success: #3e8d63;
//$info: #13101c;
//$warning: #945707;
//$danger: #d62518;
//$light: #f7f7f9;
//$dark: #343a40;
//
//// Then add them to your custom theme-colors map, together with any additional colors you might need
//$theme-colors: (
// primary: $primary,
// secondary: $secondary,
// success: $success,
// info: $info,
// warning: $warning,
// danger: $danger,
// light: $light,
// dark: $dark,
// // add any additional color below
//);
// Override whatever Bootstrap variable you want right here
// Then have Bootstrap do it's magic with these new values
@import "~bootstrap/scss/bootstrap";
@import "~@fortawesome/fontawesome-free/css/all";
html, body {
height: 100%;
/*overflow: hidden;*/
}
body {
font-family: -apple-system, Arial, 'Source Sans Pro', "Microsoft YaHei", 'Microsoft JhengHei', "WenQuanYi Micro Hei", sans-serif;
-webkit-user-select: none;
}
/*body.win32 {*/
/*background: transparent;*/
/*border-radius: 5px;*/
/*border: 1px solid #eee;*/
/*padding-right: 0 !important;*/
/*}*/
mycard {
height: 100%;
display: flex;
flex-direction: column;
}
.darwin #window-buttons, .darwin #border {
display: none;
}
.darwin #navbar {
padding-top: 1rem !important;
}
#window-buttons > i {
color: #a7a7a7;
font-size: 18px;
margin: .6rem .3rem;
}
#window-buttons > i:hover {
color: #5e5e5e;
}
#navbar {
-webkit-app-region: drag;
padding-right: 0;
flex-shrink: 0;
border-radius: initial;
}
#navbar .nav-link, #navbar .profile, #navbar i, #navbar img {
-webkit-app-region: no-drag;
}
#navbar-right > div {
float: left;
margin: 0 0.3rem;
}
/* Turn on custom 8px wide scrollbar */
.win32 ::-webkit-scrollbar {
width: 8px; /* 1px wider than Lion. */
/* This is more usable for users trying to click it. */
background-color: rgba(0, 0, 0, 0);
-webkit-border-radius: 100px;
}
/* hover effect for both scrollbar area, and scrollbar 'thumb' */
.win32 ::-webkit-scrollbar:active {
background-color: rgba(0, 0, 0, 0.05);
}
/* The scrollbar 'thumb' ...that marque oval shape in a scrollbar */
.win32 ::-webkit-scrollbar-thumb:vertical {
/* This is the EXACT color of Mac OS scrollbars.
Yes, I pulled out digital color meter */
background: rgba(0, 0, 0, 0.1);
-webkit-border-radius: 100px;
}
.win32 ::-webkit-scrollbar-thumb:vertical:active {
background: rgba(0, 0, 0, 0.2); /* Some darker color when you click it */
-webkit-border-radius: 100px;
}
.scroll {
overflow-y: hidden;
}
.scroll:hover {
//noinspection CssInvalidPropertyValue
overflow-y: overlay;
}
body.resizing::ng-deep * {
-webkit-user-select: none;
}
/*mycard {*/
/*background-color: white;*/
/*}*/
/*button, input, optgroup, select, textarea {*/
/*font-family: inherit;*/
/*}*/
#loading-bar{
-webkit-app-region: drag;
background: #f7f7f9;
padding-bottom: 4px;
}
#loading-bar>.navbar-brand{
color:#00a4d9;
font-size:24px;
width:190px;
margin: 4px 0 0 0;
text-align:center;
}
#loading-bar>.close {
color:#a7a7a7;
font-size: 18px;
float:right;
margin:17px 18px 0 0;
}
#loading-bar>.close:hover{
color:#000;
}
#loading {
height:100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction:column;
}
#loading p{
position:relative;
top:-15px;
left:10px;
font-weight: 600;
font-size: 30px;
color:#4b99ea;
text-shadow: 0 3px 8px rgba(0,0,0,0.1);
}
#loading p>span{
opacity:0;
}
#loading p>span:nth-child(1) {animation:show 2s infinite 0.0s linear;}
#loading p>span:nth-child(2) {animation:show 2s infinite 0.2s linear;}
#loading p>span:nth-child(3) {animation:show 2s infinite 0.4s linear;}
@keyframes show{
from{}
20% {opacity:0;}
21% {opacity:1;}
70% {opacity:1;}
71% {opacity:0;}
to {}
}
#loading>img{
width:300px;
}
@keyframes zhuan{
from{transform:rotate(0deg);}
to {transform:rotate(360deg);}
}
......@@ -22,6 +22,8 @@
"dom.iterable"
],
"strictPropertyInitialization": false,
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"esModuleInterop": true
},
"angularCompilerOptions": {
......
module.exports = {
target: 'electron-renderer'
target: 'electron-renderer',
externals: Object.fromEntries(['bufferutil', 'utf-8-validate'].map((pkg) => [pkg, `commonjs2 ${pkg}`])),
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment