Commit 92c66884 authored by 神楽坂玲奈's avatar 神楽坂玲奈

Merge branch 'v3' of github.com:mycard/mycard into v3

parents fb3e4ac2 70650f20
...@@ -20,13 +20,13 @@ ...@@ -20,13 +20,13 @@
<!--应用ready--> <!--应用ready-->
<div class="actions" *ngIf="currentApp.isReady() && (currentApp.id != 'ygopro')"> <div class="actions" *ngIf="currentApp.isReady() && (currentApp.id != 'ygopro')">
<button i18n *ngIf="currentApp.runable()" (click)="runApp(currentApp)" type="button" class="btn btn-primary">运行</button> <button i18n *ngIf="currentApp.runnable()" (click)="runApp(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-primary">运行</button>
<button i18n *ngIf="currentApp.runable() && currentApp.actions.get('custom')" (click)="custom(currentApp)" type="button" class="btn btn-secondary">设置</button> <button i18n *ngIf="currentApp.runnable() && currentApp.actions.get('custom')" [disabled]="!appsService.allReady(currentApp)" (click)="custom(currentApp)" type="button" class="btn btn-secondary">设置</button>
<div id="network" *ngIf="currentApp.network && currentApp.network.protocol == 'maotama'"> <div id="network" *ngIf="currentApp.network && currentApp.network.protocol == 'maotama'">
<div class="input-group"> <div class="input-group">
<input *ngIf="appsService.connections.get(currentApp)" [value]="appsService.connections.get(currentApp).address || 'Loading...'" readonly type="text" class="form-control" title="address"> <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"> <div class="input-group-btn">
<button i18n *ngIf="!appsService.connections.get(currentApp)" (click)="appsService.network(currentApp, currentApp.network.servers[0])" type="button" class="btn btn-secondary">联机</button> <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">联机</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">复制</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">复制</button>
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" style="height: 38px;"></button> <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" style="height: 38px;"></button>
<div class="dropdown-menu" [class.dropdown-menu-right]="appsService.connections.get(currentApp)"> <div class="dropdown-menu" [class.dropdown-menu-right]="appsService.connections.get(currentApp)">
...@@ -61,10 +61,10 @@ ...@@ -61,10 +61,10 @@
<th scope="row">{{i + 1}}</th> <th scope="row">{{i + 1}}</th>
<td>{{mod.name}}</td> <td>{{mod.name}}</td>
<td *ngIf="mod.isReady()"> <td *ngIf="mod.isReady()">
<button i18n type="button" (click)="uninstall(mod)" class="btn btn-danger btn-sm">卸载</button> <button i18n type="button" [disabled]="mod.isInstalled()&&!appsService.allReady(mod)" (click)="uninstall(mod)" class="btn btn-danger btn-sm">卸载</button>
</td> </td>
<td *ngIf="!mod.isInstalled()"> <td *ngIf="!mod.isInstalled()">
<button i18n (click)="installMod(mod)" type="button" *ngIf="!mod.isInstalled()" class="btn btn-primary btn-sm">安装</button> <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>
<td *ngIf="mod.isInstalled()&&!mod.isReady()"> <td *ngIf="mod.isInstalled()&&!mod.isReady()">
<progress class="progress progress-striped progress-animated" value="{{mod.status.progress}}" max="{{mod.status.total}}"></progress> <progress class="progress progress-striped progress-animated" value="{{mod.status.progress}}" max="{{mod.status.total}}"></progress>
...@@ -75,9 +75,9 @@ ...@@ -75,9 +75,9 @@
</table> </table>
</div> </div>
<h2 i18n>本地文件</h2> <h2 i18n>本地文件</h2>
<button i18n (click)="appsService.browse(currentApp)" type="button" class="btn btn-secondary">浏览本地文件</button> <button i18n (click)="appsService.browse(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-secondary">浏览本地文件</button>
<!--<button i18n type="button" (click)="verifyFiles(currentApp)" class="btn btn-secondary">校验完整性</button>--> <!--<button i18n type="button" (click)="verifyFiles(currentApp)" class="btn btn-secondary">校验完整性</button>-->
<button i18n (click)="uninstall(currentApp)" type="button" class="btn btn-secondary">卸载</button> <button i18n (click)="uninstall(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-secondary">卸载</button>
</div> </div>
<!--安装modal--> <!--安装modal-->
......
...@@ -115,7 +115,7 @@ export class App { ...@@ -115,7 +115,7 @@ export class App {
return this.status.status === "uninstalling"; return this.status.status === "uninstalling";
} }
runable(): boolean { runnable(): boolean {
return [Category.game].includes(this.category); return [Category.game].includes(this.category);
} }
......
import {Injectable, ApplicationRef, EventEmitter} from "@angular/core"; import {Injectable, ApplicationRef, EventEmitter, NgZone} from "@angular/core";
import {Http} from "@angular/http"; import {Http} from "@angular/http";
import * as crypto from "crypto"; import * as crypto from "crypto";
import {App, AppStatus, Action} from "./app"; import {App, AppStatus, Action} from "./app";
...@@ -52,11 +52,26 @@ export class AppsService { ...@@ -52,11 +52,26 @@ export class AppsService {
readonly tarPath = process.platform === "win32" ? path.join(process.env['NODE_ENV'] == 'production' ? process.resourcesPath : '', 'bin', 'bsdtar.exe') : 'bsdtar'; readonly tarPath = process.platform === "win32" ? path.join(process.env['NODE_ENV'] == 'production' ? process.resourcesPath : '', 'bin', 'bsdtar.exe') : 'bsdtar';
constructor(private http: Http, private settingsService: SettingsService, private ref: ApplicationRef, constructor(private http: Http, private settingsService: SettingsService, private ref: ApplicationRef,
private downloadService: DownloadService) { private downloadService: DownloadService, private ngZone: NgZone) {
}
get lastVisted(): App|undefined {
let id = localStorage.getItem("last_visited");
if (id) {
return this.apps.get(id);
}
return undefined;
}
set lastVisted(app: App|undefined) {
if (app) {
localStorage.setItem("last_visited", app.id);
}
} }
async loadApps() { async loadApps() {
let data = await this.http.get('./apps.json').map((response) => response.json()).toPromise(); let data = await
this.http.get('./apps.json').map((response) => response.json()).toPromise();
this.apps = this.loadAppsList(data); this.apps = this.loadAppsList(data);
return this.apps; return this.apps;
} }
...@@ -247,6 +262,21 @@ export class AppsService { ...@@ -247,6 +262,21 @@ export class AppsService {
return apps; 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 importApp(app: App, appPath: string) {
if (!app.isInstalled()) {
app.status.status = "ready";
app.local = new AppLocal();
app.local.path = appPath;
await this.update(app, true);
}
}
sha256sum(file: string): Promise<string> { sha256sum(file: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let input = fs.createReadStream(file); let input = fs.createReadStream(file);
...@@ -258,9 +288,9 @@ export class AppsService { ...@@ -258,9 +288,9 @@ export class AppsService {
reject(error); reject(error);
}); });
hash.on('readable', () => { hash.on('readable', () => {
let data = hash.read(); let data = <Buffer>hash.read();
if (data) { if (data) {
resolve((<Buffer>data).toString("hex")); resolve(data.toString("hex"));
} }
}); });
input.pipe(hash); input.pipe(hash);
...@@ -302,7 +332,7 @@ export class AppsService { ...@@ -302,7 +332,7 @@ export class AppsService {
} else { } else {
readyToUpdate = app.isReady() && mods.every((mod) => mod.isReady()); readyToUpdate = app.isReady() && mods.every((mod) => mod.isReady());
} }
if (readyToUpdate && (app.local!.version !== app.version || verify)) { if (readyToUpdate && (verify || app.local!.version !== app.version )) {
app.status.status = "updating"; app.status.status = "updating";
try { try {
Logger.info("Checking updating: ", app); Logger.info("Checking updating: ", app);
...@@ -333,6 +363,12 @@ export class AppsService { ...@@ -333,6 +363,12 @@ export class AppsService {
deletedFiles.add(file); deletedFiles.add(file);
} }
} }
// changedFiles包含addedFiles,addedFiles仅供mod更新的时候使用。
for (let addedFile of addedFiles) {
changedFiles.add(addedFile);
}
let backupFiles: string[] = []; let backupFiles: string[] = [];
let restoreFiles: string[] = []; let restoreFiles: string[] = [];
if (app.parent) { if (app.parent) {
...@@ -743,12 +779,19 @@ export class AppsService { ...@@ -743,12 +779,19 @@ export class AppsService {
let option = task.option; let option = task.option;
let installDir = option.installDir; let installDir = option.installDir;
let checksumFile = await this.getChecksumFile(app); 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) { if (app.parent) {
// mod需要安装到parent路径 // mod需要安装到parent路径
installDir = app.parent.local!.path; installDir = app.parent.local!.path;
let parentFiles = new ComparableSet(Array.from(app.parent.local!.files.keys())); let parentFiles = new ComparableSet(Array.from(app.parent.local!.files.keys()));
let appFiles = new ComparableSet(Array.from(checksumFile.keys())); let appFiles = new ComparableSet(Array.from(checksumFile.keys()));
let conflictFiles = appFiles.intersection(parentFiles); let conflictFiles = appFiles.intersection(parentFiles);
app.status.total += conflictFiles.size;
if (conflictFiles.size > 0) { if (conflictFiles.size > 0) {
let backupPath = path.join(option.installLibrary, "backup", app.parent.id); let backupPath = path.join(option.installLibrary, "backup", app.parent.id);
// 文件夹不需要备份,删除 // 文件夹不需要备份,删除
...@@ -757,18 +800,23 @@ export class AppsService { ...@@ -757,18 +800,23 @@ export class AppsService {
conflictFiles.delete(conflictFile); conflictFiles.delete(conflictFile);
} }
} }
await this.backupFiles(app.parent.local!.path, backupPath, conflictFiles); 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();
} catch (e) {
reject(e);
}
});
});
} }
} }
let allFiles = new Set(checksumFile.keys());
app.status.status = "installing";
app.status.total = allFiles.size;
app.status.progress = 0;
// let timeNow = new Date().getTime(); // let timeNow = new Date().getTime();
for (let file of option.downloadFiles) { for (let file of option.downloadFiles) {
await this.createDirectory(installDir); await this.createDirectory(installDir);
let interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
this.extract(file, installDir).subscribe( this.extract(file, installDir).subscribe(
(lastItem: string) => { (lastItem: string) => {
...@@ -782,8 +830,8 @@ export class AppsService { ...@@ -782,8 +830,8 @@ export class AppsService {
resolve(); resolve();
}); });
}); });
clearInterval(interval);
} }
clearInterval(interval);
await this.postInstall(app, installDir); await this.postInstall(app, installDir);
console.log("post install success"); console.log("post install success");
let local = new AppLocal(); let local = new AppLocal();
...@@ -903,7 +951,8 @@ export class AppsService { ...@@ -903,7 +951,8 @@ export class AppsService {
} }
} }
async backupFiles(dir: string, backupDir: string, files: Iterable<string>) { async backupFiles(dir: string, backupDir: string, files: Iterable<string>, callback?: (progress: number)=>void) {
let n = 0;
for (let file of files) { for (let file of files) {
await new Promise(async(resolve, reject) => { await new Promise(async(resolve, reject) => {
let srcPath = path.join(dir, file); let srcPath = path.join(dir, file);
...@@ -912,19 +961,28 @@ export class AppsService { ...@@ -912,19 +961,28 @@ export class AppsService {
fs.unlink(backupPath, (err) => { fs.unlink(backupPath, (err) => {
fs.rename(srcPath, backupPath, resolve); fs.rename(srcPath, backupPath, resolve);
}); });
if (callback) {
callback(n)
}
n += 1;
}); });
} }
} }
async restoreFiles(dir: string, backupDir: string, files: Iterable<string>) { async restoreFiles(dir: string, backupDir: string, files: Iterable<string>, callback?: (progress: number)=>{}) {
let n = 0;
for (let file of files) { for (let file of files) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
let backupPath = path.join(backupDir, file); let backupPath = path.join(backupDir, file);
let srcPath = path.join(dir, file); let srcPath = path.join(dir, file);
fs.unlink(srcPath, (err) => { fs.unlink(srcPath, (err) => {
fs.rename(backupPath, srcPath, resolve); fs.rename(backupPath, srcPath, resolve);
}) });
}) n += 1;
if (callback) {
callback(n);
}
});
} }
} }
...@@ -993,18 +1051,21 @@ export class AppsService { ...@@ -993,18 +1051,21 @@ export class AppsService {
console.error('doUninstall', "无法卸载,还有依赖此程序的游戏。", app); console.error('doUninstall', "无法卸载,还有依赖此程序的游戏。", app);
throw "无法卸载,还有依赖此程序的游戏。" throw "无法卸载,还有依赖此程序的游戏。"
} }
app.status.status = "uninstalling"; app.status.status = "uninstalling";
let appDir = app.local!.path; let appDir = app.local!.path;
let files = Array.from(app.local!.files.keys()).sort().reverse(); let files = Array.from(app.local!.files.keys()).sort().reverse();
app.status.total = files.length; 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) { for (let file of files) {
app.status.progress += 1; app.status.progress += 1;
await this.deleteFile(path.join(appDir, file)); await this.deleteFile(path.join(appDir, file));
} }
if (app.parent) { if (app.parent) {
// TODO: 建立Library模型,把拼路径的事情交给Library // TODO: 建立Library模型,把拼路径的事情交给Library
let backupDir = path.join(path.dirname(appDir), "backup", app.parent.id); let backupDir = path.join(path.dirname(appDir), "backup", app.parent.id);
...@@ -1015,7 +1076,15 @@ export class AppsService { ...@@ -1015,7 +1076,15 @@ export class AppsService {
await this.restoreFiles(appDir, backupDir, Array.from(difference)) await this.restoreFiles(appDir, backupDir, Array.from(difference))
} }
} }
resolve();
}
catch (e) {
reject(e);
}
});
});
clearInterval(interval);
app.reset() app.reset()
} }
} }
\ No newline at end of file
...@@ -31,10 +31,10 @@ export class LobbyComponent implements OnInit { ...@@ -31,10 +31,10 @@ export class LobbyComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.apps = await this.appsService.loadApps(); this.apps = await this.appsService.loadApps();
await this.appsService.migrate(); await this.appsService.migrate();
for(let app of this.apps.values()) { for (let app of this.apps.values()) {
this.appsService.update(app); this.appsService.update(app);
} }
this.chooseApp(Array.from(this.apps.values()).find(app => app.isInstalled()) || this.apps.get("ygopro")!); this.chooseApp(this.appsService.lastVisted || this.apps.get("ygopro")!);
// 初始化聊天室 // 初始化聊天室
let url = new URL('candy/index.html', location.href); let url = new URL('candy/index.html', location.href);
...@@ -42,7 +42,7 @@ export class LobbyComponent implements OnInit { ...@@ -42,7 +42,7 @@ export class LobbyComponent implements OnInit {
params.set('jid', this.loginService.user.username + '@mycard.moe'); params.set('jid', this.loginService.user.username + '@mycard.moe');
params.set('password', this.loginService.user.external_id.toString()); params.set('password', this.loginService.user.external_id.toString());
params.set('nickname', this.loginService.user.username); params.set('nickname', this.loginService.user.username);
switch(this.settingsService.getLocale()){ switch (this.settingsService.getLocale()) {
case 'zh-CN': case 'zh-CN':
params.set('language', 'cn'); params.set('language', 'cn');
break; break;
...@@ -57,6 +57,7 @@ export class LobbyComponent implements OnInit { ...@@ -57,6 +57,7 @@ export class LobbyComponent implements OnInit {
chooseApp(app: App) { chooseApp(app: App) {
this.currentApp = app; this.currentApp = app;
this.appsService.lastVisted = app;
if (this.candy && this.currentApp.conference) { if (this.candy && this.currentApp.conference) {
(<WebViewElement>this.candy.nativeElement).send('join', this.currentApp.conference + '@conference.mycard.moe'); (<WebViewElement>this.candy.nativeElement).send('join', this.currentApp.conference + '@conference.mycard.moe');
} }
......
...@@ -5,19 +5,19 @@ ...@@ -5,19 +5,19 @@
<option *ngFor="let deck of decks" [ngValue]="deck">{{deck}}</option> <option *ngFor="let deck of decks" [ngValue]="deck">{{deck}}</option>
</select> </select>
</div> </div>
<button i18n type="submit" class="btn btn-secondary" (click)="edit_deck(current_deck)">编辑</button> <button i18n [disabled]="!appsService.allReady(app)" type="submit" class="btn btn-secondary" (click)="edit_deck(current_deck)">编辑</button>
<button i18n type="submit" (click)="delete_deck(current_deck)" class="btn btn-secondary">删除</button> <button i18n [disabled]="!appsService.allReady(app)" type="submit" (click)="delete_deck(current_deck)" class="btn btn-secondary">删除</button>
<button i18n types="submit" (click)="refresh()" class="btn btn-secondary">刷新</button> <button i18n [disabled]="!appsService.allReady(app)" type="submit" (click)="refresh()" class="btn btn-secondary">刷新</button>
</form> </form>
<div class="actions"> <div class="actions">
<button i18n (click)="request_match('athletic')" *ngIf="matching_arena != 'athletic'" [disabled]="matching" type="button" class="btn btn-primary">竞技匹配</button> <button i18n [disabled]="matching||!appsService.allReady(app)" (click)="request_match('athletic')" *ngIf="matching_arena != 'athletic'" type="button" class="btn btn-primary">竞技匹配</button>
<button i18n (click)="cancel_match()" *ngIf="matching_arena == 'athletic'" type="button" class="btn btn-primary">取消等待</button> <button i18n [disabled]="!appsService.allReady(app)" (click)="cancel_match()" *ngIf="matching_arena == 'athletic'" type="button" class="btn btn-primary">取消等待</button>
<button i18n (click)="request_match('entertain')" *ngIf="matching_arena != 'entertain'" [disabled]="matching" type="button" class="btn btn-secondary">娱乐匹配</button> <button i18n [disabled]="matching||!appsService.allReady(app)" (click)="request_match('entertain')" *ngIf="matching_arena != 'entertain'" type="button" class="btn btn-secondary">娱乐匹配</button>
<button i18n (click)="cancel_match()" *ngIf="matching_arena == 'entertain'" type="button" class="btn btn-secondary">取消等待</button> <button i18n [disabled]="!appsService.allReady(app)" (click)="cancel_match()" *ngIf="matching_arena == 'entertain'" type="button" class="btn btn-secondary">取消等待</button>
<button i18n [disabled]="matching" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-create-modal">创建房间</button> <button i18n [disabled]="matching||!appsService.allReady(app)" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-create-modal">创建房间</button>
<button i18n [disabled]="matching" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-list-modal">房间列表</button> <button i18n [disabled]="matching||!appsService.allReady(app)" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-list-modal">房间列表</button>
<button i18n type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-create-windbot">单人模式</button> <button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-create-windbot">单人模式</button>
</div> </div>
<div class="modal fade" id="game-create-windbot" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal fade" id="game-create-windbot" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
......
...@@ -15,6 +15,7 @@ import {Http, Headers, URLSearchParams} from "@angular/http"; ...@@ -15,6 +15,7 @@ import {Http, Headers, URLSearchParams} from "@angular/http";
import "rxjs/Rx"; import "rxjs/Rx";
import {ISubscription} from "rxjs/Subscription"; import {ISubscription} from "rxjs/Subscription";
import {SettingsService} from "./settings.sevices"; import {SettingsService} from "./settings.sevices";
import {AppsService} from "./apps.service";
declare const $: any; declare const $: any;
...@@ -108,7 +109,7 @@ export class YGOProComponent implements OnInit { ...@@ -108,7 +109,7 @@ export class YGOProComponent implements OnInit {
connections: WebSocket[] = []; connections: WebSocket[] = [];
constructor(private http: Http, private settingsService: SettingsService, private loginService: LoginService, private ref: ChangeDetectorRef) { constructor(private http: Http, private appsService: AppsService, private loginService: LoginService, private ref: ChangeDetectorRef) {
switch (process.platform) { switch (process.platform) {
case 'darwin': case 'darwin':
this.numfont = ['/System/Library/Fonts/SFNSTextCondensed-Bold.otf']; this.numfont = ['/System/Library/Fonts/SFNSTextCondensed-Bold.otf'];
......
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