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

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

parents fb3e4ac2 70650f20
......@@ -20,13 +20,13 @@
<!--应用ready-->
<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.runable() && currentApp.actions.get('custom')" (click)="custom(currentApp)" type="button" class="btn btn-secondary">设置</button>
<button i18n *ngIf="currentApp.runnable()" (click)="runApp(currentApp)" [disabled]="!appsService.allReady(currentApp)" type="button" class="btn btn-primary">运行</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 class="input-group">
<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">
<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 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)">
......@@ -61,10 +61,10 @@
<th scope="row">{{i + 1}}</th>
<td>{{mod.name}}</td>
<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 *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 *ngIf="mod.isInstalled()&&!mod.isReady()">
<progress class="progress progress-striped progress-animated" value="{{mod.status.progress}}" max="{{mod.status.total}}"></progress>
......@@ -75,9 +75,9 @@
</table>
</div>
<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 (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>
<!--安装modal-->
......
......@@ -115,7 +115,7 @@ export class App {
return this.status.status === "uninstalling";
}
runable(): boolean {
runnable(): boolean {
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 * as crypto from "crypto";
import {App, AppStatus, Action} from "./app";
......@@ -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';
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() {
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);
return this.apps;
}
......@@ -247,6 +262,21 @@ export class AppsService {
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> {
return new Promise((resolve, reject) => {
let input = fs.createReadStream(file);
......@@ -258,9 +288,9 @@ export class AppsService {
reject(error);
});
hash.on('readable', () => {
let data = hash.read();
let data = <Buffer>hash.read();
if (data) {
resolve((<Buffer>data).toString("hex"));
resolve(data.toString("hex"));
}
});
input.pipe(hash);
......@@ -302,7 +332,7 @@ export class AppsService {
} else {
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";
try {
Logger.info("Checking updating: ", app);
......@@ -333,6 +363,12 @@ export class AppsService {
deletedFiles.add(file);
}
}
// changedFiles包含addedFiles,addedFiles仅供mod更新的时候使用。
for (let addedFile of addedFiles) {
changedFiles.add(addedFile);
}
let backupFiles: string[] = [];
let restoreFiles: string[] = [];
if (app.parent) {
......@@ -743,12 +779,19 @@ export class AppsService {
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);
// 文件夹不需要备份,删除
......@@ -757,18 +800,23 @@ export class AppsService {
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();
for (let file of option.downloadFiles) {
await this.createDirectory(installDir);
let interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => {
this.extract(file, installDir).subscribe(
(lastItem: string) => {
......@@ -782,8 +830,8 @@ export class AppsService {
resolve();
});
});
clearInterval(interval);
}
clearInterval(interval);
await this.postInstall(app, installDir);
console.log("post install success");
let local = new AppLocal();
......@@ -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) {
await new Promise(async(resolve, reject) => {
let srcPath = path.join(dir, file);
......@@ -912,19 +961,28 @@ export class AppsService {
fs.unlink(backupPath, (err) => {
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) {
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);
}
});
}
}
......@@ -993,18 +1051,21 @@ export class AppsService {
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);
......@@ -1015,7 +1076,15 @@ export class AppsService {
await this.restoreFiles(appDir, backupDir, Array.from(difference))
}
}
resolve();
}
catch (e) {
reject(e);
}
});
});
clearInterval(interval);
app.reset()
}
}
\ No newline at end of file
......@@ -31,10 +31,10 @@ export class LobbyComponent implements OnInit {
async ngOnInit() {
this.apps = await this.appsService.loadApps();
await this.appsService.migrate();
for(let app of this.apps.values()) {
for (let app of this.apps.values()) {
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);
......@@ -42,7 +42,7 @@ export class LobbyComponent implements OnInit {
params.set('jid', this.loginService.user.username + '@mycard.moe');
params.set('password', this.loginService.user.external_id.toString());
params.set('nickname', this.loginService.user.username);
switch(this.settingsService.getLocale()){
switch (this.settingsService.getLocale()) {
case 'zh-CN':
params.set('language', 'cn');
break;
......@@ -57,6 +57,7 @@ export class LobbyComponent implements OnInit {
chooseApp(app: App) {
this.currentApp = app;
this.appsService.lastVisted = app;
if (this.candy && this.currentApp.conference) {
(<WebViewElement>this.candy.nativeElement).send('join', this.currentApp.conference + '@conference.mycard.moe');
}
......
......@@ -5,19 +5,19 @@
<option *ngFor="let deck of decks" [ngValue]="deck">{{deck}}</option>
</select>
</div>
<button i18n 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 types="submit" (click)="refresh()" class="btn btn-secondary">刷新</button>
<button i18n [disabled]="!appsService.allReady(app)" type="submit" class="btn btn-secondary" (click)="edit_deck(current_deck)">编辑</button>
<button i18n [disabled]="!appsService.allReady(app)" type="submit" (click)="delete_deck(current_deck)" class="btn btn-secondary">删除</button>
<button i18n [disabled]="!appsService.allReady(app)" type="submit" (click)="refresh()" class="btn btn-secondary">刷新</button>
</form>
<div class="actions">
<button i18n (click)="request_match('athletic')" *ngIf="matching_arena != 'athletic'" [disabled]="matching" 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 (click)="request_match('entertain')" *ngIf="matching_arena != 'entertain'" [disabled]="matching" 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]="matching" 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 type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-create-windbot">单人模式</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 [disabled]="!appsService.allReady(app)" (click)="cancel_match()" *ngIf="matching_arena == 'athletic'" type="button" class="btn btn-primary">取消等待</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 [disabled]="!appsService.allReady(app)" (click)="cancel_match()" *ngIf="matching_arena == 'entertain'" type="button" class="btn btn-secondary">取消等待</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||!appsService.allReady(app)" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-list-modal">房间列表</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#game-create-windbot">单人模式</button>
</div>
<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";
import "rxjs/Rx";
import {ISubscription} from "rxjs/Subscription";
import {SettingsService} from "./settings.sevices";
import {AppsService} from "./apps.service";
declare const $: any;
......@@ -108,7 +109,7 @@ export class YGOProComponent implements OnInit {
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) {
case 'darwin':
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