Commit 8736dfa4 authored by 神楽坂玲奈's avatar 神楽坂玲奈

refactor

parent 49433c4b
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "mycard"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico",
{ "glob": "**/*", "input": "../node_modules/candy/res", "output": "res" }
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "mycard",
"styles": [
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {
"spec": false
},
"directive": {
"spec": false
},
"class": {
"spec": false
},
"guard": {
"spec": false
},
"module": {
"spec": false
},
"pipe": {
"spec": false
},
"service": {
"spec": false
}
}
}
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
/bin/ # See http://help.github.com/ignore-files/ for more about ignoring files.
/app/**/*.js
/app/**/*.js.map /bin
/app/*.metadata.json /*.js
/app/*.shim.ts /*.js.map
/app/*.ngfactory.ts /result/*.js
/aot /result/*.js.map
!/aot/index.html !/common.js
/node_modules/ /public
/dist/
/cache/ # compiled output
/typings/ /dist
/npm-debug.log* /tmp
/.idea/ /out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
messages.xlf
messages.xlf.bak
locale/*.xlf.bak
/package-lock.json
language: node_js
node_js: node
os:
- linux
- osx
dist: trusty
sudo: required
env:
npm_config_target: 1.4.15
npm_config_arch: x64
npm_config_target_arch: x64
npm_config_disturl: https://atom.io/download/atom-shell
npm_config_runtime: electron
npm_config_build_from_source: true
global:
secure: KsebO9wNxM2RfUGg6Y0E4hRdXzQLNe1fdB1AOV5U1LddLGZYTYvsknPL6oyjOV3vY5ed7wueErt1GCDjEZJMdox0rMUEZ9HH8umwUoJi2uS6LoaU31yWNCCbPbpdtJw3rohzNvEtxd1Y01U5msKPuUd4M5mt/RKlPPPR/L5H178=
addons:
apt:
packages:
- icnsutils
- graphicsmagick
- xz-utils
cache:
directories:
- node_modules
- $HOME/.electron
- $HOME/.cache
before_install:
- env
- openssl aes-256-cbc -K $encrypted_9f35b7f09ebe_key -iv $encrypted_9f35b7f09ebe_iv
-in ssh-key.enc -out $HOME/.ssh/id_ecdsa -d
- chmod 600 $HOME/.ssh/id_ecdsa
install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mkdir -p bin; curl --location --retry
5 https://github.com/aria2/aria2/releases/download/release-1.29.0/aria2-1.29.0-osx-darwin.tar.bz2
| tar --strip-components=2 -C bin -jxf - aria2-1.29.0/bin/aria2c; fi
- npm install
- npm prune
script:
- npm run dist
<!--<h2>MyCard 招募公告</h2>-->
<!--<p>MyCard 伴随大家已经有 6 年了,在这 6 年间 MyCard 作为一个同人平台很感谢得到大家的支持,现在 MyCard 为了给支持的大家带来更好的体验,正在努力进行全新的改版的开发工作,希望可以得到大家的支持和帮助。</p>--><!--<p>职位:(前端)开发工程师</p>--><!--<p>负责平台客户端的开发,及网站和论坛相关的改版工作。</p>--><!--<p>职位描述:对 ACG 领域有一定的了解,会js等编程领域的专业技能,对软件开发具有一定的热情和自主能动性,认真严谨和团队意识。</p>--><!--<p>联系邮箱:hr@mycard.moe</p>--><!--<p>工作地点:上海市长宁区(工资面议)</p>-->
<!--<h2>联系我们</h2>--><!--<dl>--><!--<dt>应聘</dt>--><!--<dd>hr@mycard.com</dd>--><!--<dt>投稿、合作、侵权投诉</dt>--><!--<dd>business@mycard.com</dd>--><!--<dt>问题反馈</dt>--><!--<dd>support@mycard.moe</dd>--><!--</dl>-->
/**
* Created by zh99998 on 16/9/2.
*/
import {Component} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'about',
templateUrl: 'about.component.html',
styleUrls: ['about.component.css'],
})
export class AboutComponent {
}
: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;
}
#purchase-form .form-check {
padding-right: 8px;
}
#purchase-form legend {
font-size: 1rem;
margin-bottom: 0;
margin-top: .5rem;
}
\ No newline at end of file
<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-toggle="modal" data-target="#purchase-modal">{{currentApp.price.cny | currency:'CNY':true}} 购买</button>
<button i18n type="button" (click)="updateInstallOption(currentApp)" class="btn btn-secondary btn-sm" data-toggle="modal" data-target="#install-modal">安装试玩版</button>
<!--<button i18n (click)="updateInstallOption(currentApp)" type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#install-modal">我已经购买过</button>-->
</div>
<!--应用已购买,未安装-->
<div *ngIf="currentApp.isBought() && !currentApp.isInstalled()" class="i-b">
<button i18n (click)="updateInstallOption(currentApp)" type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#install-modal">安装</button>
<button i18n *ngIf="currentApp.runnable()" (click)="updateInstallOption(currentApp)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-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.id != 'ygopro')" 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 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-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.id != 'ygopro') && currentApp && currentApp.network && currentApp.network.protocol == 'maotama'" [currentApp]="currentApp"></network>
<ygopro *ngIf="currentApp.isReady() && (currentApp.id == 'ygopro')" [app]="currentApp" [currentApp]="currentApp" (points)="onPoints($event)"></ygopro>
</div>
</div>
<div id="arena" class="panel panel-default" *ngIf="currentApp.id === 'ygopro' && points ">
<h2 i18n>排位成绩</h2>
<table class="table table-sm">
<tbody>
<tr>
<th i18n>竞技排名</th>
<td>{{points.arena_rank}}</td>
<th i18n>娱乐排名</th>
<td>{{points.exp_rank}}</td>
</tr>
<tr>
<th i18n>竞技胜率</th>
<td>{{points.athletic_wl_ratio}}%</td>
<th i18n>经验</th>
<td>{{points.exp}}</td>
</tr>
<tr>
<th i18n>胜场</th>
<td>{{points.athletic_win}}</td>
<th i18n>胜场</th>
<td>{{points.entertain_win}}</td>
</tr>
<tr>
<th i18n>负场</th>
<td>{{points.athletic_lose}}</td>
<th i18n>负场</th>
<td>{{points.entertain_lose}}</td>
</tr>
<tr>
<th i18n>平局</th>
<td>{{points.athletic_draw}}</td>
<th i18n>平局</th>
<td>{{points.entertain_draw}}</td>
</tr>
<tr>
<th i18n>总场</th>
<td>{{points.athletic_all}}</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-danger 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>
</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 *ngIf="mods && mods.length">
<table class="table table-striped">
<thead class="thead-inverse">
<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>{{mod.name}}</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" *ngIf="installOption">
<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="close" data-dismiss="modal" aria-label="Close">
<span>&times;</span>
</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 == '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-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" *ngIf="installOption">
<div class="modal-dialog" role="document">
<form id="import-form" class="modal-content" (ngSubmit)="importGame(currentApp,installOption,referencesInstall)" #theForm="ngForm">
<div class="modal-header">
<h5 i18n class="modal-title">导入 {{currentApp.name}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p i18n>选择主程序 {{currentApp.actions.get('main').execute}}</p>
<label class="custom-file" lang="en">
<input (click)="$event.preventDefault();selectImport(currentApp)" type="file" id="file" class="custom-file-input">
<span class="custom-file-control">{{import_path || currentApp.actions.get('main').execute}}</span>
</label>
<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-dismiss="modal">取消</button>
<button i18n type="submit" [disabled]="import_path && !theForm.form.valid" 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="close" data-dismiss="modal" aria-label="Close">
<span>&times;</span>
</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-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="close" data-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-dismiss="modal">返回</button>
</div>
</div>
</div>
</div>
\ No newline at end of file
import {ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
import {AppsService} from './apps.service';
import {InstallOption} from './install-option';
import {SettingsService} from './settings.sevices';
import {App} from './app';
import {DownloadService} from './download.service';
import {clipboard, remote} from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import * as $ from 'jquery';
import {Points} from './ygopro.component';
import {Http} from '@angular/http';
import {LoginService} from './login.service';
declare const Notification: any;
// declare interface Window {
// adsbygoogle: any[];
// }
//
// declare var adsbygoogle: any[];
@Component({
moduleId: module.id,
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: Points;
tags: {};
payment = 'alipay';
creating_order = false;
constructor(private appsService: AppsService, private settingsService: SettingsService,
private downloadService: DownloadService, private ref: ChangeDetectorRef, private el: ElementRef,
private http: Http, private loginService: LoginService) {
this.tags = this.settingsService.getLocale().startsWith('zh') ? {
'recommend': '推荐',
'mysterious': '迷之物体',
'touhou': '东方 Project',
'touhou_pc98': '东方旧作',
'language': '语言包'
} : {
'recommend': 'Recommended',
'mysterious': 'Something',
'touhou': 'Touhou Project',
'touhou_pc98': 'Touhou old series',
'language': 'Language Pack'
};
}
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';
}
// 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();
});
});
}
}
updateInstallOption(app: App) {
this.installOption = new InstallOption(app);
this.installOption.installLibrary = this.settingsService.getDefaultLibrary().path;
this.references = Array.from(app.references.values());
console.log(this.references);
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: string) {
this.appsService.runApp(app, action_name);
}
custom(app: App) {
this.appsService.runApp(app, 'custom');
}
async importGame(targetApp: App, option: InstallOption, referencesInstall: { [id: string]: boolean }) {
$('#import-modal').modal('hide');
let dir = path.dirname(this.import_path);
// 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: Points) {
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://api.mycard.moe/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;
}
}
import { ApplicationRef, EventEmitter, Injectable, NgZone } from '@angular/core';
import { Http } from '@angular/http';
import * as child_process from 'child_process';
import { ChildProcess } from 'child_process';
import * as crypto from 'crypto';
import { remote } from 'electron';
import * as sudo from 'electron-sudo';
import * as fs from 'fs';
import * as glob from 'glob';
import * as ini from 'ini';
import * as path from 'path';
import * as readline from 'readline';
import 'rxjs/Rx';
import { Observable, Observer } from 'rxjs/Rx';
import { Action, App, AppStatus } from './app';
import { AppLocal } from './app-local';
import { DownloadService, DownloadStatus } from './download.service';
import { InstallOption } from './install-option';
import { LoginService } from './login.service';
import { SettingsService } from './settings.sevices';
import { ComparableSet } from './shared/ComparableSet';
import Timer = NodeJS.Timer;
import ReadableStream = NodeJS.ReadableStream;
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()
export class AppsService {
private apps: Map<string, App>;
eventEmitter = new EventEmitter<void>();
map: Map<string, string> = new Map();
connections = new Map<App, Connection>();
maotama: Promise<ChildProcess>;
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 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://api.mycard.moe/apps.json';
let keysURL = 'https://api.mycard.moe/keys';
try {
let data = await this.http.get(appsURL).map((response) => response.json()).toPromise();
let keys_data = await this.http.get(keysURL, {
search: {
user_id: this.loginService.user.email
}
}).map((response) => response.json()).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) {
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 {
app[key] = null;
}
}
}
// 时间
if (app.released_at) {
app.released_at = new Date(app.released_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();
});
});
}
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();
} 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: Error) => {
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();
});
});
}
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();
} 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();
});
});
}
// 遍历寻找旧版本与新版本不一样的文件和新版本比旧版少了的文件
// 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);
let metalink = await this.http.post(updateUrl, changedFiles).map((response) => response.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();
});
});
}
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();
} 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);
_app.status.status = 'downloading';
let metalink = await this.http.get(metalinkUrl).map((response) => response.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);
let cwd = (<AppLocal>app.local).path;
let action: Action = <Action>app.actions.get(action_name);
let args: string[] = [];
let env = {};
for (let child of children) {
if (child.isInstalled()) {
let _action = child.actions.get(action_name);
if (_action) {
action = _action;
}
}
}
let execute = path.join(cwd, action.execute);
if (app.id === 'th123') {
let th105 = <App>app.references.get('th105');
if (th105.isInstalled()) {
const config_file = path.join((<AppLocal>app.local).path, 'configex123.ini');
let config = await new Promise((resolve, reject) => {
fs.readFile(config_file, {encoding: 'utf-8'}, (error, data) => {
if (error) {
return reject(error);
}
resolve(ini.parse(data));
});
});
config['th105path'] = {path: (<AppLocal>th105.local).path};
await new Promise((resolve, reject) => {
fs.writeFile(config_file, ini.stringify(config), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
}
if (action.open) {
let np2 = action.open;
let openAction: Action;
openAction = np2.actions.get('main')!;
let openPath = np2.local!.path;
if (action.open.id === 'np2fmgen') {
const config_file = path.join(action.open!.local!.path, 'np21nt.ini');
let config = await new Promise((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:', app.local!.path, action.execute);
config['NekoProject21']['fontfile'] =
path.win32.join(process.platform === 'win32' ? '' : 'Z:', app.local!.path, 'font.bmp');
await new Promise((resolve, reject) => {
fs.writeFile(config_file, ini.stringify(config), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
if (process.platform !== 'win32') {
args.push(openAction.execute);
args = args.concat(openAction.args);
let wine = openAction.open!;
openPath = wine.local!.path;
openAction = openAction!.open!.actions.get('main')!;
}
cwd = np2.local!.path;
}
args = args.concat(openAction.args);
args.push(action.execute);
execute = path.join(openPath, openAction.execute);
env = Object.assign(env, openAction.env);
}
args = args.concat(action.args);
env = Object.assign(env, action.env);
console.log(execute, args, env, cwd);
let handle = child_process.spawn(execute, args, {env: env, cwd: cwd});
handle.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
handle.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
handle.on('close', (code) => {
console.log(`child process exited with code ${code}`);
remote.getCurrentWindow().restore();
});
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 = sudo.fork('maotama', [], {stdio: ['inherit', 'inherit', 'inherit', 'ipc']});
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();
} 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();
});
});
}
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>) => {
Logger.info('Start to extract... Command Line: ' + this.tarPath, file, dir);
let tarProcess = child_process.spawn(this.tarPath, ['xvf', file, '-C', dir]);
let rl = readline.createInterface({
input: <ReadableStream>tarProcess.stderr,
});
rl.on('line', (input: string) => {
observer.next(input.split(' ', 2)[1]);
});
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) => {
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);
return this.http.get(checksumUrl)
.map((response) => {
let map = new Map<string, string>();
for (let line of response.text().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();
} 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>
/**
* Created by zh99998 on 16/9/2.
*/
let shadow: ShadowRoot;
const $ = require('../jquery-shadow.js');
$.fn.init = new Proxy($.fn.init, {
construct(target, argumentsList, newTarget) {
let [selector, context, root] = argumentsList;
if (shadow) {
if (selector === 'body') {
selector = shadow;
} else if (selector === document) {
selector = $('#candy');
} else if (!context) {
context = shadow;
}
}
return new target(selector, context, root);
}
});
window['jQuery'] = $;
import {Component, ViewEncapsulation, OnInit, Input, OnChanges, SimpleChanges, ElementRef} from '@angular/core';
import {LoginService} from './login.service';
import {SettingsService} from './settings.sevices';
import {App} from './app';
import 'node_modules/candy/libs.min.js';
import 'node_modules/candy/candy.min.js';
import 'node_modules/candy-shop/notifyme/candy.js';
import 'node_modules/candy-shop/namecomplete/candy.js';
import 'node_modules/candy-shop/modify-role/candy.js';
import 'node_modules/candy-shop/me-does/candy.js';
import 'node_modules/candy-shop/notifications/candy.js';
import 'node_modules/candy-shop/refocus/candy.js';
delete window['jQuery'];
// Candy fix
declare const Candy: any, CandyShop: any, Base64: any;
Base64.encode = (data: string) => Buffer.from(data).toString('base64');
Base64.decode = (data: string) => Buffer.from(data, 'base64').toString();
Candy.Util.getPosLeftAccordingToWindowBounds = new Proxy(Candy.Util.getPosLeftAccordingToWindowBounds, {
apply(target, thisArg, argumentsList) {
argumentsList[1] -= shadow.host.getBoundingClientRect().left;
return target.apply(thisArg, argumentsList);
}
});
Candy.Util.getPosTopAccordingToWindowBounds = new Proxy(Candy.Util.getPosTopAccordingToWindowBounds, {
apply(target, thisArg, argumentsList) {
argumentsList[1] -= shadow.host.getBoundingClientRect().top;
return target.apply(thisArg, argumentsList);
}
});
// 性能优化:禁用加入动画
Candy.View.Pane.Roster.joinAnimation = function () {
};
// 性能优化:禁用用户排序
declare const Mustache: any;
Candy.View.Pane.Roster._insertUser = function (roomJid: string, roomId: string, user: any, userId: string, currentUser: any) {
let contact = user.getContact();
let html = Mustache.to_html(Candy.View.Template.Roster.user, {
roomId: roomId,
userId: userId,
userJid: user.getJid(),
realJid: user.getRealJid(),
status: user.getStatus(),
contact_status: contact ? contact.getStatus() : 'unavailable',
nick: user.getNick(),
displayNick: Candy.Util.crop(user.getNick(), Candy.View.getOptions().crop.roster.nickname),
role: user.getRole(),
affiliation: user.getAffiliation(),
me: currentUser !== undefined && user.getNick() === currentUser.getNick(),
tooltipRole: $.i18n._('tooltipRole'),
tooltipIgnored: $.i18n._('tooltipIgnored')
});
let rosterPane = Candy.View.Pane.Room.getPane(roomJid, '.roster-pane');
rosterPane.append(html);
};
// 性能优化:将未读消息计数的的 jQuery show() 改为直接置 style
Candy.View.Pane.Chat.increaseUnreadMessages = function (roomJid: string) {
let unreadElem = this.getTab(roomJid).find('.unread');
unreadElem.text(unreadElem.text() !== '' ? parseInt(unreadElem.text(), 10) + 1 : 1);
unreadElem[0].style.display = 'inherit';
// only increase window unread messages in private chats
if (Candy.View.Pane.Chat.rooms[roomJid].type === 'chat' || Candy.View.getOptions().updateWindowOnAllMessages === true) {
Candy.View.Pane.Window.increaseUnreadMessages();
}
};
// 性能优化:将收到消息时的滚动放进requestIdleCallback
declare const requestIdleCallback: Function;
Candy.View.Pane.Message.
show = function (roomJid: any, name: any, message: any, xhtmlMessage: any, timestamp: any, from: any, carbon: any, stanza: any) {
message = Candy.Util.Parser.all(message.substring(0, Candy.View.getOptions().crop.message.body));
if (Candy.View.getOptions().enableXHTML === true && xhtmlMessage) {
xhtmlMessage = Candy.Util.parseAndCropXhtml(xhtmlMessage, Candy.View.getOptions().crop.message.body);
}
timestamp = timestamp || new Date();
// Assume we have an ISO-8601 date string and convert it to a Date object
if (!timestamp.toDateString) {
timestamp = Candy.Util.iso8601toDate(timestamp);
}
// Before we add the new message, check to see if we should be automatically scrolling or not.
let messagePane = Candy.View.Pane.Room.getPane(roomJid, '.message-pane');
let enableScroll;
if (stanza && stanza.children('delay').length > 0) {
enableScroll = true;
} else {
enableScroll =
messagePane.scrollTop() + messagePane.outerHeight() === messagePane.prop('scrollHeight') || !$(messagePane).is(':visible');
}
Candy.View.Pane.Chat.rooms[roomJid].enableScroll = enableScroll;
let evtData: any = {
roomJid: roomJid,
name: name,
message: message,
xhtmlMessage: xhtmlMessage,
from: from,
stanza: stanza
};
if ($(Candy).triggerHandler('candy:view.message.before-show', evtData) === false) {
return;
}
message = evtData.message;
xhtmlMessage = evtData.xhtmlMessage;
if (xhtmlMessage !== undefined && xhtmlMessage.length > 0) {
message = xhtmlMessage;
}
if (!message) {
return;
}
let renderEvtData = {
template: Candy.View.Template.Message.item,
templateData: {
name: name,
displayName: Candy.Util.crop(name, Candy.View.getOptions().crop.message.nickname),
message: message,
time: Candy.Util.localizedTime(timestamp),
timestamp: timestamp.toISOString(),
roomjid: roomJid,
from: from
},
stanza: stanza
};
$(Candy).triggerHandler('candy:view.message.before-render', renderEvtData);
let html = Mustache.to_html(renderEvtData.template, renderEvtData.templateData);
Candy.View.Pane.Room.appendToMessagePane(roomJid, html);
let elem = Candy.View.Pane.Room.getPane(roomJid, '.message-pane').children().last();
// click on username opens private chat
elem.find('a.label').click(function (event: any) {
event.preventDefault();
// Check if user is online and not myCandy.View.Pane
let room = Candy.Core.getRoom(roomJid);
if (room &&
name !== Candy.View.Pane.Room.getUser(Candy.View.getCurrent().roomJid).getNick() &&
room.getRoster().get(roomJid + '/' + name)) {
if (Candy.View.Pane.PrivateRoom.open(roomJid + '/' + name, name, true) === false) {
return false;
}
}
});
if (!carbon) {
let notifyEvtData = {
name: name,
displayName: Candy.Util.crop(name, Candy.View.getOptions().crop.message.nickname),
roomJid: roomJid,
message: message,
time: Candy.Util.localizedTime(timestamp),
timestamp: timestamp.toISOString()
};
$(Candy).triggerHandler('candy:view.message.notify', notifyEvtData);
// Check to see if in-core notifications are disabled
if (!Candy.Core.getOptions().disableCoreNotifications) {
if (Candy.View.getCurrent().roomJid !== roomJid || !Candy.View.Pane.Window.hasFocus()) {
Candy.View.Pane.Chat.increaseUnreadMessages(roomJid);
if (!Candy.View.Pane.Window.hasFocus()) {
// Notify the user about a new private message OR on all messages if configured
if (Candy.View.Pane.Chat.rooms[roomJid].type === 'chat' || Candy.View.getOptions().updateWindowOnAllMessages === true) {
Candy.View.Pane.Chat.Toolbar.playSound();
}
}
}
}
if (Candy.View.getCurrent().roomJid === roomJid) {
requestIdleCallback(function () {
Candy.View.Pane.Room.scrollToBottom(roomJid);
});
}
}
evtData.element = elem;
$(Candy).triggerHandler('candy:view.message.after-show', evtData);
};
document['__defineGetter__']('cookie', () => 'candy-nostatusmessages');
document['__defineSetter__']('cookie', () => true);
declare const Strophe: any;
declare const $iq: any;
@Component({
moduleId: module.id,
selector: 'candy',
templateUrl: 'candy.component.html',
styleUrls: ['candy.component.css'],
encapsulation: ViewEncapsulation.Native
})
export class CandyComponent implements OnInit, OnChanges {
@Input()
currentApp: App;
jid: string;
password: string;
nickname: string;
// ismin_window:Boolean=false;
// ismax_window:Boolean=false;
height_default_window: string = '230px';
constructor(private loginService: LoginService, private settingsService: SettingsService, private element: ElementRef) {
}
ngOnInit() {
this.jid = this.loginService.user.username + '@mycard.moe';
this.password = this.loginService.user.external_id.toString();
this.nickname = this.loginService.user.username;
shadow = this.element.nativeElement.shadowRoot;
// 很 Tricky 的加载 Candy 的 css,这里涉及图片等资源的相对路径引用问题,如果丢给 Angular 去加载,会让相对路径找不到
const element = document.createElement('style');
element.innerHTML = `
@import "node_modules/font-awesome/css/font-awesome.min.css";
@import "node_modules/candy/libs.min.css";
@import "node_modules/candy/res/default.css";
@import "node_modules/candy-shop/notifyme/candy.css";
@import "node_modules/candy-shop/namecomplete/candy.css";
@import "node_modules/candy-shop/modify-role/candy.css";
`;
shadow.insertBefore(element, shadow.firstChild);
Candy.View.Template.Login.form = `
<form method="post" id="login-form" class="login-form">
<input type="hidden" id="nickname" name="nickname" value="${this.nickname}"/>
{{#displayUsername}}
<input type="hidden" id="username" name="username" value="${this.jid}"/>
{{#displayDomain}}
<span class="at-symbol">@</span>
<select id="domain" name="domain">{{#domains}}<option value="{{domain}}">{{domain}}</option>{{/domains}}</select>
{{/displayDomain}}
{{/displayUsername}}
{{#presetJid}}<input type="hidden" id="username" name="username" value="{{presetJid}}"/>{{/presetJid}}
{{#displayPassword}}<input type="hidden" id="password" name="password" value="${this.password}"/>{{/displayPassword}}
<input type="submit" class="button" value="{{_loginSubmit}}" />
</form>
`;
Candy.init('wss://chat.mycard.moe:5280/websocket', {
core: {
debug: false,
autojoin: this.currentApp.conference && [this.currentApp.conference + '@conference.mycard.moe'],
resource: 'mycard-' + Math.random().toString().split('.')[1]
},
view: {
assets: 'node_modules/candy/res/',
language: this.settingsService.getLocale().startsWith('zh') ? 'cn' : 'en',
enableXHTML: true,
}
});
CandyShop.NotifyMe.init();
CandyShop.NameComplete.init();
CandyShop.ModifyRole.init();
CandyShop.MeDoes.init();
CandyShop.Notifications.init();
CandyShop.Refocus.init();
Candy.Core.connect(this.jid, this.password, this.nickname);
// $(Candy).on('candy:core:roster:loaded', (event: JQueryEventObject, data: any) => {
// this.roster = Object.values(data.roster.getAll());
// });
// $(Candy).on('candy:core:roster:fetched', (event: JQueryEventObject, data: any) => {
// this.roster = Object.values(data.roster.getAll());
// });
// $(Candy).on('candy:core:roster:removed', (event: JQueryEventObject, data: any) => {
// this.roster = Object.values(Candy.Core.getRoster().getAll());
// });
// $(Candy).on('candy:core:roster:added', (event: JQueryEventObject, data: any) => {
// this.roster = Object.values(Candy.Core.getRoster().getAll());
// });
// $(Candy).on('candy:core:roster:updated', (event: JQueryEventObject, data: any) => {
// this.roster = Object.values(Candy.Core.getRoster().getAll());
// });
}
ngOnChanges(changes: SimpleChanges): void {
if (!Candy.Core.getConnection()) {
return;
}
let conference = changes['currentApp'].currentValue.conference;
if (!conference) {
return;
}
conference += '@conference.mycard.moe';
try {
if (Candy.View.Pane.Chat.rooms[conference]) {
Candy.View.Pane.Room.show(conference);
} else {
Candy.Core.Action.Jabber.Room.Join(conference);
}
} catch (error) {
}
}
minimize(): void {
// let minimize:HTMLElement = $('#minimize')[0];
// let maximized:HTMLElement = $('#maximized')[0];
// let un_minimize:HTMLElement = $('#un_minimize')[0];
// let un_maximized:HTMLElement = $('#un_maximized')[0];
$('#candy').attr('data-minormax', 'min');
document.getElementById('candy-wrapper')!.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();
}
restore(): void {
$('#candy').attr('data-minormax', 'default');
document.getElementById('candy-wrapper')!.style!.height = this.height_default_window;
$('#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();
}
maximize(): void {
$('#candy').attr('data-minormax', 'max');
document.getElementById('candy-wrapper')!.style!.height = 'calc( 100% - 180px )' ;
$('#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').show();
$('#maximize').hide();
}
}
/**
* Created by zh99998 on 2017/6/1.
*/
import * as 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);
}
}
\ No newline at end of file
import {TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID} from '@angular/core';
import {remote} from 'electron';
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
}
: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: 14px;
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: 14px;
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;*/
/*}*/
\ No newline at end of file
<!-- Begin page content -->
<div #nav id="nav-wrapper" class="resize-wrapper resize-right">
<nav id="apps" *ngIf="apps" class="bg-faded sidebar scroll">
<div id="search" class="input-group">
<i class="fa fa-search input-group-addon search" id="basic-addon1"></i>
<input #search id="search-input" type="text" class="form-control search" placeholder="搜索游戏" aria-describedby="basic-addon1">
</div>
<span i18n *ngIf="grouped_apps.installed">已安装</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="nav-link" [class.active]="app===currentApp" [href]="'https://mycard.moe/' + app.id">
<img *ngIf="app.icon" class="icon" [src]="app.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="pie" [class.second-half]="app.status.progress/app.status.total>0.5">
<div class="left-side half-circle" [style.transform]="'rotate('+(app.status.progress/app.status.total).toString()+'turn)'"></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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.recommend">推荐</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.mysterious">迷之物体</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.touhou">东方 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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.touhou_pc98">东方旧作</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.runtime_installed">已安装的运行库</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
</nav>
<div class="resize" (mousedown)="mousedown($event)"></div>
</div>
<div id="right">
<div id="main">
<app-detail class="scroll" *ngIf="currentApp" [currentApp]="currentApp"></app-detail>
<roster class="scroll"></roster>
</div>
<div id="candy-wrapper" class="resize-wrapper resize-top" style="max-height: calc( 100% - 180px )">
<div class="resize" (mousedown)="mousedown($event)"></div>
<candy *ngIf="currentApp" [currentApp]="currentApp"></candy>
</div>
</div>
<div id="right-shadow"></div>
\ No newline at end of file
/**
* 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.service';
import { App, Category } from './app';
import { shell } from 'electron';
import { SettingsService } from './settings.sevices';
const ReconnectingWebSocket = require('reconnecting-websocket');
// import 'typeahead.js';
// import Options = Twitter.Typeahead.Options;
@Component({
moduleId: module.id,
selector: 'lobby',
templateUrl: 'lobby.component.html',
styleUrls: ['lobby.component.css'],
})
export class LobbyComponent implements OnInit {
currentApp: App;
private apps: Map<string, App>;
resizing: HTMLElement | undefined;
offset: number;
@ViewChild('search')
search: ElementRef;
private messages: WebSocket;
constructor(private appsService: AppsService, private loginService: LoginService,
private settingsService: SettingsService, private ref: ChangeDetectorRef) {
}
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://api.moecube.com: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;
}
}
chooseApp(app: App) {
this.currentApp = app;
this.appsService.lastVisited = app;
}
get grouped_apps() {
let contains = ['game', 'music', 'book'].map((value) => Category[value]);
let result = {runtime: []};
for (let app of this.apps.values()) {
let tag: string;
if (contains.includes(app.category)) {
if (app.isInstalled()) {
tag = 'installed';
} else {
tag = app.tags ? app.tags[0] : 'test';
}
} else {
if (app.isInstalled()) {
tag = 'runtime_installed';
} else {
tag = 'runtime';
}
}
if (!result[tag]) {
result[tag] = [];
}
result[tag].push(app);
}
return result;
}
openExternal(url: string) {
shell.openExternal(url);
}
}
<webview [src]="url" (will-navigate)="return_sso($event.url)" (did-get-redirect-request)="return_sso($event.newURL)" (new-window)="openExternal($event.url)"></webview>
\ No newline at end of file
/**
* Created by zh99998 on 16/9/2.
*/
import { Component } from '@angular/core';
import { LoginService } from './login.service';
import * as crypto from 'crypto';
import { shell } from 'electron';
@Component({
moduleId: module.id,
selector: 'login',
templateUrl: 'login.component.html',
styleUrls: ['login.component.css'],
})
export class LoginComponent {
url: string;
readonly return_sso_url = 'https://mycard.moe/login_callback'; // 这个url不会真的被使用,可以填写不存在的
constructor(private loginService: LoginService) {
let params = new URLSearchParams();
params.set('return_sso_url', this.return_sso_url);
let payload = Buffer.from(params.toString()).toString('base64');
let url = new URL('https://accounts.moecube.com');
params = url['searchParams'];
params.set('sso', payload);
params.set('sig', crypto.createHmac('sha256', 'zsZv6LXHDwwtUAGa').update(payload).digest('hex'));
this.url = url.toString();
if (this.loginService.logging_out) {
url = new URL('https://ygobbs.com/logout');
params = url['searchParams'];
// params.set('redirect', this.url);
// 暂时 hack 一下登出,因为聊天室现在没办法重新初始化,于是登出后刷新页面。
params.set('redirect', 'https://mycard.moe/logout_callback');
this.url = url.toString();
}
}
return_sso(return_url: string) {
if (return_url === 'https://mycard.moe/logout_callback') {
return location.reload();
}
if (!return_url.startsWith(this.return_sso_url)) {
return;
}
let token = new URL(return_url)['searchParams'].get('sso');
if (!token) {
return;
}
let user = this.toObject(new URLSearchParams(Buffer.from(token, 'base64').toString()));
this.loginService.login(user);
}
toObject(entries: Iterable<[string, any]>): any {
let result = {};
for (let [key, value] of entries) {
result[key] = value;
}
return result;
}
openExternal(url: string) {
shell.openExternal(url);
}
}
/**
* Created by zh99998 on 2016/10/25.
*/
import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
export interface User {
admin: boolean;
avatar_url: string;
email: string;
external_id: number;
moderator: boolean;
name: string;
username: string;
}
@Injectable()
export class LoginService {
user: User;
logged_in = false;
logging_out = false;
constructor (private http: Http) {
let data = localStorage.getItem('login');
if (data) {
this.user = JSON.parse(data);
this.logged_in = true;
}
}
login (user: User) {
this.user = user;
this.logged_in = true;
localStorage.setItem('login', JSON.stringify(user));
}
logout () {
this.logging_out = true;
this.logged_in = false;
localStorage.removeItem('login');
}
}
// import {MyCardNgFactory} from '../aot/app/mycard.module.ngfactory';
// import {getTranslationProviders} from './i18n-providers';
// import {enableProdMode} from '@angular/core';
// import {platformBrowser} from '@angular/platform-browser';
// enableProdMode();
//
// getTranslationProviders().then(providers => {
// const options = {providers};
// platformBrowser().bootstrapModuleFactory(MyCardNgFactory);
// });
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {getTranslationProviders} from './i18n-providers';
import {MyCard} from './mycard.module';
getTranslationProviders().then(providers => {
const options = {providers};
platformBrowserDynamic().bootstrapModule(MyCard, options);
});
/*: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 .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;
}
\ No newline at end of file
<nav id="navbar" class="navbar navbar-toggleable-md navbar-light">
<a id="navbar-brand" class="navbar-brand" href="#">MyCard</a>
<ul class="navbar-nav mr-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">
<div id="update-status">
<i #error [hidden]="update_status != 'error'" (click)="update_retry()" class="fa fa-exclamation-circle" data-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-toggle="tooltip" i18n-title title="正在检查更新"></i>
<i #update_available [hidden]="update_status != 'update-available'" class="fa fa-refresh fa-spin" data-toggle="tooltip" i18n-title title="正在下载更新"></i>
<i #update_downloaded [hidden]="update_status != 'update-downloaded'" (click)="update_install()" class="fa fa-angle-double-up" data-toggle="tooltip" i18n-title title="下载更新完成,点击安装"></i>
</div>
<div 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-toggle="modal" data-target="#settings-modal" class="fa fa-cog item-icon" aria-hidden="true" i18n-title title="设置"></i>
</div>
<div id="border">|</div>
<div 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>
</div>
</div>
</nav>
<login class="page" *ngIf="!loginService.logged_in"></login>
<store class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'store'"></store>
<lobby class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'lobby'"></lobby>
<webview class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'community'" src="https://ygobbs.com" (new-window)="openExternal($event.url)"></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 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="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</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-dismiss="modal">取消</button>
<button i18n type="submit" class="btn btn-primary">确定</button>
</div>
</form>
</div>
</div>
</div>
import { ChangeDetectorRef, Component, ElementRef, OnInit, Renderer, ViewChild } from '@angular/core';
import 'bootstrap';
import { remote, shell } from 'electron';
import * as $ from 'jquery';
import * as Tether from 'tether';
import { LoginService } from './login.service';
import { SettingsService } from './settings.sevices';
window['Tether'] = Tether;
const autoUpdater: Electron.AutoUpdater = remote.getGlobal('autoUpdater');
@Component({
moduleId: module.id,
selector: 'mycard',
templateUrl: 'mycard.component.html',
styleUrls: ['mycard.component.css'],
})
export class MyCardComponent implements OnInit {
currentPage: string = 'lobby';
update_status: string | undefined = remote.getGlobal('update_status');
update_error: string | undefined;
currentWindow = remote.getCurrentWindow();
window = window;
@ViewChild('error')
error: ElementRef;
@ViewChild('checking_for_update')
checking_for_update: ElementRef;
@ViewChild('update_available')
update_available: ElementRef;
@ViewChild('update_downloaded')
update_downloaded: ElementRef;
update_elements: Map<string, ElementRef>;
locale: string;
resizing: HTMLElement | null;
@ViewChild('moesound')
moesound: ElementRef;
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
}));
// document.addEventListener('drop', (event)=>{
// console.log('drop', event);
// event.preventDefault();
//
// });
}
constructor(private renderer: Renderer, private 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());
autoUpdater.on('error', (error) => {
this.set_update_status('error');
});
autoUpdater.on('checking-for-update', () => {
this.set_update_status('checking-for-update');
});
autoUpdater.on('update-available', () => {
this.set_update_status('update-available');
});
autoUpdater.on('update-not-available', () => {
this.set_update_status('update-not-available');
});
autoUpdater.on('update-downloaded', (event) => {
this.set_update_status('update-downloaded');
});
this.locale = this.settingsService.getLocale();
}
update_retry() {
autoUpdater.checkForUpdates();
}
update_install() {
autoUpdater.quitAndInstall();
}
set_update_status(status: string) {
console.log('autoUpdater', status);
if (this.update_status) {
let element = this.update_elements.get(this.update_status);
if (element) {
$(element.nativeElement).tooltip('dispose');
}
}
this.update_status = status;
this.ref.detectChanges();
let element = this.update_elements.get(this.update_status);
if (element) {
$(element.nativeElement).tooltip({ placement: 'bottom', container: 'body' });
}
}
openExternal(url: string) {
shell.openExternal(url);
}
submit() {
if (this.locale !== this.settingsService.getLocale()) {
localStorage.setItem(SettingsService.SETTING_LOCALE, this.locale);
remote.app.relaunch();
remote.app.quit();
}
}
//
// moesound_loaded() {
// this.moesound.nativeElement.insertCSS(`
// body > section > header, #bjax-target > div.row.m-t-lg.m-b-lg, #bjax-target > section {
// display: none;
// }
// body > section > section {
// top: 0!important;
// }
// `);
// }
//
// moesound_newwindow(url: string) {
// console.log(url);
// }
}
import { ErrorHandler, LOCALE_ID, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { MyCardComponent } from './mycard.component';
import { LoginComponent } from './login.component';
import { StoreComponent } from './store.component';
import { LobbyComponent } from './lobby.component';
import { AppDetailComponent } from './app-detail.component';
import { RosterComponent } from './roster.component';
import { YGOProComponent } from './ygopro.component';
import { AppsService } from './apps.service';
import { SettingsService } from './settings.sevices';
import { LoginService } from './login.service';
import { DownloadService } from './download.service';
import { AboutComponent } from './about.component';
import { CandyComponent } from './candy.component';
import { RavenErrorHandler } from './error-handler';
import { NetworkComponent } from './network.component';
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, HttpModule],
declarations: [
MyCardComponent, LoginComponent, StoreComponent, LobbyComponent,
AppDetailComponent, RosterComponent, YGOProComponent, AboutComponent, CandyComponent, NetworkComponent
],
bootstrap: [MyCardComponent],
providers: [
AppsService, SettingsService, LoginService, DownloadService
, {
provide: LOCALE_ID,
deps: [SettingsService],
useFactory: (settingsService: SettingsService) => settingsService.getLocale()
}
// , {
// provide: ErrorHandler, useClass: RavenErrorHandler
// }
],
schemas: [NO_ERRORS_SCHEMA]
})
export class MyCard {
}
#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;
}
\ No newline at end of file
<div id="network" *ngIf="currentApp.network && currentApp.network.protocol == 'maotama'">
<!--<button (click)="log(appsService)">test</button>-->
<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-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>
\ No newline at end of file
import { ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges, Injectable } from '@angular/core';
import { AppsService } from './apps.service';
import {App} from './app';
@Component({
moduleId: module.id,
selector: 'network',
templateUrl: 'network.component.html',
styleUrls: ['network.component.css'],
})
@Injectable()
export class NetworkComponent {
@Input()
currentApp: App;
constructor(private appsService: AppsService) {
console.log( 'constructor' );
}
}
:host {
position: fixed;
top: 0;
right: 0;
background: white;
}
#friend_list > ul > li > div > p {
font-size: 12px;
padding: 3px 5px;
margin: 0;
cursor: default;
}
#friend_list > ul > li > img {
border-radius: 25.5px;
}
#friend_list > ul > li {
padding: 3px;
list-style-type: none;
position: relative;
}
#friend_list > ul > li:hover {
padding: 3px;
list-style-type: none;
position: relative;
background: #aaa;
}
#friend_list > ul > li > i {
position: absolute;
top: 38px;
left: 38px;
}
#friend_list > ul {
padding: 0;
}
.fl {
float: left;
}
#friend_list i {
font-size: 10px;
}
.red {
color: red;
}
.green_light {
color: #8f8;
}
.red_light {
color: #f88;
}
.grey {
color: grey;
}
.blue_light {
color: #88f;
}
\ No newline at end of file
<!--<link href="roster.component.css" type="text/css" rel="stylesheet">--><!--<link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css">--><!--<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">--><!--<script src="../node_modules/core-js/client/shim.min.js"></script>-->
<!--<script src="../node_modules/zone.js/dist/zone.js"></script>--><!--<script src="../node_modules/reflect-metadata/Reflect.js"></script>--><!--<script src="../node_modules/systemjs/dist/system.src.js"></script>-->
<!--<script src="../systemjs.config.js"></script>-->
<!--<div>-->
<!--<div class="input-group" id="friend_search">-->
<!--<div class="input-group ">-->
<!--<i class="fa fa-search input-group-addon search" id="basic-addon1"></i>-->
<!--<input type="text" class="form-control" aria-label="Amount (to the nearest dollar)">-->
<!--&lt;!&ndash;<span class="input-group-addon">+</span>&ndash;&gt;-->
<!--<button type="button" class="input-group-addon btn btn-sm btn-primary blue_light">+</button>-->
<!--</div>-->
<!--</div>-->
<!--<div id="friend_list">-->
<!--<ul>-->
<!--<li *ngFor="let contact of roster">-->
<!--<img class="fl" src="https://ygobbs.com//letter_avatar_proxy/v2/letter/q/cdc98d/45.png">-->
<!--<i class="fa fa-circle green_light"></i>-->
<!--<div class="fl">-->
<!--<p>{{contact.getName()}}</p>-->
<!--<p *ngIf="contact.getSubscription() != 'both'">等待确认</p>-->
<!--<p *ngIf="contact.getSubscription() == 'both'">{{contact.getStatus()}}</p>-->
<!--</div>-->
<!--<div style="clear: both"></div>-->
<!--</li>-->
<!--<li>-->
<!--<img class="fl" src="https://ygobbs.com//letter_avatar_proxy/v2/letter/q/cdc98d/45.png">-->
<!--<i class="fa fa-circle green_light"></i>-->
<!--<div class="fl">-->
<!--<p>我叫什么名来着</p>-->
<!--<p>发呆ing</p>-->
<!--</div>-->
<!--<div style="clear: both"></div>-->
<!--</li>-->
<!--<li>-->
<!--<img class="fl" src="https://ygobbs.com/user_avatar/ygobbs.com/sky%E7%A5%9E%E6%99%BA/45/16501_1.png">-->
<!--<i class="fa fa-play-circle red_light"></i>-->
<!--<div class="fl">-->
<!--<p>我叫什么名来着</p>-->
<!--<p class="red">东方妖妖梦</p>-->
<!--</div>-->
<!--<div style="clear: both"></div>-->
<!--</li>-->
<!--<li>-->
<!--<img class="fl" src="https://ygobbs.com/user_avatar/ygobbs.com/%E6%98%9F%E5%85%89pokeboy/45/10237_1.png">-->
<!--<i class="fa fa-question-circle blue_light"></i>-->
<!--<div class="fl">-->
<!--<p>我叫什么名来着</p>-->
<!--<p>发呆ing</p>-->
<!--</div>-->
<!--<div style="clear: both"></div>-->
<!--</li>-->
<!--<li>-->
<!--<img class="fl" src="https://ygobbs.com//user_avatar/ygobbs.com/%E7%B1%B3%E7%B1%B3%E7%B1%B3%E5%BE%B7%E6%8B%89%E4%BB%80/45/17187_1.png">-->
<!--<i class="fa fa-clock-o grey"></i>-->
<!--<div class="fl">-->
<!--<p>我叫什么名来着</p>-->
<!--<p>发呆ing</p>-->
<!--</div>-->
<!--<div style="clear: both"></div>-->
<!--</li>-->
<!--</ul>-->
<!--</div>-->
<!--</div>-->
/**
* Created by zh99998 on 16/9/2.
*/
import {Component, Input, EventEmitter, Output, OnInit, OnChanges} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'roster',
templateUrl: 'roster.component.html',
styleUrls: ['roster.component.css'],
})
export class RosterComponent implements OnInit, OnChanges {
@Input()
roster: any;
@Output()
chat = new EventEmitter<string>();
ngOnInit() {
// console.log(this.roster);
}
ngOnChanges() {
// console.log(this.roster);
}
}
/**
* Created by weijian on 2016/10/24.
*/
import {Injectable} from '@angular/core';
import {remote} from 'electron';
import * as path from 'path';
export interface Library {
'default': boolean;
path: string;
}
@Injectable()
export class SettingsService {
static SETTING_LIBRARY = 'library';
static defaultLibraries = [
{
'default': true,
path: path.join(remote.app.getPath('appData'), 'MyCardLibrary')
},
];
static SETTING_LOCALE = 'locale';
static defaultLocale = remote.app.getLocale();
locale: string;
libraries: Library[];
getLibraries () {
if (!this.libraries) {
let data = localStorage.getItem(SettingsService.SETTING_LIBRARY);
if (!data) {
this.libraries = SettingsService.defaultLibraries;
localStorage.setItem(SettingsService.SETTING_LIBRARY,
JSON.stringify(SettingsService.defaultLibraries));
} else {
this.libraries = JSON.parse(data);
}
}
return this.libraries;
}
addLibrary (libraryPath: string, isDefault: boolean) {
let libraries = this.getLibraries();
if (isDefault) {
libraries.forEach((l) => {
l.default = false;
});
}
libraries.push({'default': isDefault, path: libraryPath});
this.libraries = libraries;
localStorage.setItem(SettingsService.SETTING_LIBRARY, JSON.stringify(libraries));
}
setDefaultLibrary (library: Library) {
let libraries = this.getLibraries();
libraries.forEach((l) => {
l.default = library.path === l.path;
});
this.libraries = libraries;
localStorage.setItem(SettingsService.SETTING_LIBRARY, JSON.stringify(libraries));
}
getDefaultLibrary (): Library {
if (!this.libraries) {
this.getLibraries();
}
let result = this.libraries.find((item) => item.default === true);
if (result) {
return result;
} else {
throw('no default library found');
}
}
getLocale (): string {
if (!this.locale) {
let locale = localStorage.getItem(SettingsService.SETTING_LOCALE);
if (!locale) {
this.locale = SettingsService.defaultLocale;
localStorage.setItem(SettingsService.SETTING_LOCALE, SettingsService.defaultLocale);
} else {
this.locale = locale;
}
}
return this.locale;
}
setLocale (locale: string) {
this.locale = locale;
localStorage.setItem(SettingsService.SETTING_LOCALE, locale);
}
}
[{"__symbolic":"module","version":3,"metadata":{"ComparableSet":{"__symbolic":"class","extends":{"__symbolic":"reference","name":"Set"},"members":{"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"error","message":"Could not resolve type","line":4,"character":25,"context":{"typeName":"Iterable"}}]}],"isSuperset":[{"__symbolic":"method"}],"union":[{"__symbolic":"method"}],"intersection":[{"__symbolic":"method"}],"difference":[{"__symbolic":"method"}]}}}},{"__symbolic":"module","version":1,"metadata":{"ComparableSet":{"__symbolic":"class","extends":{"__symbolic":"reference","name":"Set"},"members":{"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"error","message":"Could not resolve type","line":4,"character":25,"context":{"typeName":"Iterable"}}]}],"isSuperset":[{"__symbolic":"method"}],"union":[{"__symbolic":"method"}],"intersection":[{"__symbolic":"method"}],"difference":[{"__symbolic":"method"}]}}}}]
\ No newline at end of file
store
\ No newline at end of file
/**
* Created by zh99998 on 16/9/2.
*/
import {Component} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'store',
templateUrl: 'store.component.html',
styleUrls: ['store.component.css'],
})
export class StoreComponent {
}
<div *ngIf="!matching" id="action">
<button [disabled]="!appsService.allReady(app)" (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)" (click)="request_match('entertain')" type="button" class="btn btn-secondary btn-sm">娱乐匹配</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-target="#game-list-modal">自定义游戏</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-target="#game-create-windbot">单人模式</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-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="input-group input-group-sm">
<label i18n class="input-group-addon" id="basic-addon1">卡组</label>
<select class="form-control form-control-sm" id="exampleSelect1" name="deck" [(ngModel)]="current_deck">
<option *ngFor="let deck of decks" [ngValue]="deck">{{deck}}</option>
</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 class="modal fade" id="game-create-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="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</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-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" 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-toggle="tooltip" data-placement="bottom" [title]="user.username">
</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 *ngIf="room.options.rule != default_options.rule">{{{'0': 'OCG', '1': 'TCG', '2': 'O/T', '3': '专有卡禁止'}[room.options.rule]}}</span>
<span i18n *ngIf="room.options.start_lp != default_options.start_lp">{{room.options.start_lp}} LP</span>
<span i18n *ngIf="room.options.start_hand != default_options.start_hand">{{room.options.start_hand}} 初始</span>
<span i18n *ngIf="room.options.draw_count != default_options.draw_count">{{room.options.draw_count}} 抽卡</span>
<span i18n *ngIf="room.options.enable_priority != default_options.enable_priority">旧规则</span>
<span i18n *ngIf="room.options.no_check_deck != default_options.no_check_deck">不检查</span>
<span i18n *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck">不洗卡</span>
</td>
</tr>
</tbody>
</table>
<div id="join-private" class="input-group input-group-sm">
<i class="fa fa-key input-group-addon" aria-hidden="true"></i>
<input [(ngModel)]="join_password" type="text" class="form-control" placeholder="在这输入你朋友的私密房间密码就可以进去了哦!">
<span class="input-group-btn"><button class="btn btn-secondary" type="button" (click)="join_private(join_password)">加入私密房间</button></span>
</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 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 id="copy-wrapper" class="input-group-btn" data-toggle="tooltip" title="房间密码已复制到剪贴板">
<button class="btn btn-secondary fa fa-clipboard" type="button" title="复制" (click)="copy(host_password, $event)"></button>
</span>
</div>
<small 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">OCG & TCG</option>
<option i18n value="3">专有卡禁止</option>
</select>
</div>
<div class="form-group">
<label i18n for="game-create-rule">决斗模式</label>
<select class="form-control form-control-sm" id="game-create-mode" name="mode" (change)="room.options.start_lp = room.options.mode == 2 ? 16000 : 8000" [(ngModel)]="room.options.mode">
<option i18n value="0">单局模式</option>
<option i18n value="1">比赛模式</option>
<option i18n value="2">TAG</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>
</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="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</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="close" data-dismiss="modal" aria-label="Close">-->
<!--<span aria-hidden="true">&times;</span>-->
<!--</button>-->
<!--</div>-->
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs" role="tablist">
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-toggle="tab" href="#home" role="tab">推荐</a>-->
<!--</li>-->
<li class="nav-item">
<a i18n class="nav-link active" data-toggle="tab" href="#game-replay-watch" role="tab">观战</a>
</li>
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-toggle="tab" href="#home" role="tab">收藏的录像</a>-->
<!--</li>-->
<li class="nav-item">
<a i18n class="nav-link" data-toggle="tab" href="#game-replay-local" role="tab">本地录像</a>
</li>
<li *ngIf="settingsService.getLocale().startsWith('zh')" class="nav-item">
<a class="nav-link" data-toggle="tab" href="#game-replay-bilibili" role="tab">哔哩哔哩</a>
</li>
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-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="title">
<span i18n>游戏模式</span>
<div id="watch-filter" class="dropdown">
<button i18n class="btn btn-secondary dropdown-toggle btn-sm" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
筛选
</button>
<div class="dropdown-menu">
<h6 i18n class="dropdown-header">匹配</h6>
<div class="form-check dropdown-item">
<input id="filter-athletic" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.athletic" (change)="refresh_replay_rooms()">
<label i18n for="filter-athletic" class="form-check-label">竞技匹配</label>
</div>
<div class="form-check dropdown-item">
<input id="filter-entertain" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.entertain" (change)="refresh_replay_rooms()">
<label i18n for="filter-entertain" class="form-check-label">娱乐匹配</label>
</div>
<h6 i18n class="dropdown-header">自定义游戏</h6>
<div class="form-check dropdown-item">
<input id="filter-single" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.single" (change)="refresh_replay_rooms()">
<label i18n for="filter-single" class="form-check-label">单局模式</label>
</div>
<div class="form-check dropdown-item">
<input id="filter-match" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.match" (change)="refresh_replay_rooms()">
<label i18n for="filter-match" class="form-check-label">比赛模式</label>
</div>
<div class="form-check dropdown-item">
<input id="filter-tag" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.tag" (change)="refresh_replay_rooms()">
<label i18n for="filter-tag" class="form-check-label">TAG</label>
</div>
<h6 i18n class="dropdown-header">单人模式</h6>
<div class="form-check dropdown-item">
<input id="filter-windbot" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.windbot" (change)="refresh_replay_rooms()">
<label i18n for="filter-windbot" class="form-check-label">单人模式</label>
</div>
</div>
</div>
</th>
<th i18n class="users">游戏标题</th>
<th i18n class="mode">玩家</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 *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-toggle="tooltip" data-placement="bottom" [title]="user.username">
</td>
<td class="extra">
<span *ngIf="!(room.arena || room.id.startsWith('AI#'))">
<span *ngIf="room.options.rule != default_options.rule">{{{'0': 'OCG', '1': 'TCG', '2': 'O/T', '3': '专有卡禁止'}[room.options.rule]}}</span>
<span i18n *ngIf="room.options.start_lp != default_options.start_lp">{{room.options.start_lp}} LP</span>
<span i18n *ngIf="room.options.start_hand != default_options.start_hand">{{room.options.start_hand}} 初始</span>
<span i18n *ngIf="room.options.draw_count != default_options.draw_count">{{room.options.draw_count}} 抽卡</span>
<span i18n *ngIf="room.options.enable_priority != default_options.enable_priority">旧规则</span>
<span i18n *ngIf="room.options.no_check_deck != default_options.no_check_deck">不检查</span>
<span i18n *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck">不洗卡</span>
</span>
</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-dismiss="modal">Close</button>-->
<!--&lt;!&ndash;<button type="button" class="btn btn-primary">Save changes</button>&ndash;&gt;-->
<!--</div>-->
</div>
</div>
</div>
\ No newline at end of file
/**
* Created by zh99998 on 16/9/2.
*/
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { Headers, Http } from '@angular/http';
import * as child_process from 'child_process';
import { clipboard, remote, shell } from 'electron';
import * as fs from 'fs-extra';
import * as ini from 'ini';
import { EncodeOptions } from 'ini';
import * as $ from 'jquery';
import * as path from 'path';
import 'rxjs/Rx';
import { ISubscription } from 'rxjs/Subscription';
import { App } from './app';
import { AppsService } from './apps.service';
import { LoginService } from './login.service';
import { SettingsService } from './settings.sevices';
import Timer = NodeJS.Timer;
// import WillNavigateEvent = Electron.WillNavigateEvent;
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;
url?: string;
address: string;
port: number;
custom?: boolean;
replay?: boolean;
}
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_hand: number;
draw_count: number;
enable_priority: boolean;
no_check_deck: boolean;
no_shuffle_deck: boolean;
lflist?: number;
time_limit?: number;
}
export interface Points {
exp: number;
exp_rank: number;
pt: number;
arena_rank: number;
win: number;
lose: number;
draw: number;
all: number;
ratio: number;
}
interface YGOProData {
windbot: { [locale: string]: string[] };
}
let matching: ISubscription | 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<Points> = new EventEmitter();
decks: string[] = [];
replays: string[] = [];
current_deck: string;
system_conf: string;
numfont: string[];
textfont: string[];
@ViewChild('bilibili')
bilibili: ElementRef;
@ViewChild('youtube')
youtube: ElementRef;
// points: Points;
windbot: string[]; // ["琪露诺", "谜之剑士LV4", "复制植物", "尼亚"];
servers: Server[] = [];
rooms_loading = true;
default_options: Options = {
mode: 1,
rule: this.settingsService.getLocale().startsWith('zh') ? 0 : 1,
start_lp: 8000,
start_hand: 5,
draw_count: 1,
enable_priority: false,
no_check_deck: false,
no_shuffle_deck: false,
lflist: 0,
time_limit: 180
};
room: Room = { title: this.loginService.user.username + '的房间', options: Object.assign({}, this.default_options) };
rooms: 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: ISubscription | 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: Http, private appsService: AppsService, private loginService: LoginService,
public settingsService: SettingsService, private ref: ChangeDetectorRef) {
switch (process.platform) {
case 'darwin':
this.numfont = ['/System/Library/Fonts/SFNSTextCondensed-Bold.otf'];
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);
}
if (this.settingsService.getLocale().startsWith('zh')) {
this.servers.push({
id: 'tiramisu',
url: 'wss://tiramisu.mycard.moe:7923',
address: '112.124.105.11',
port: 7911,
custom: true,
replay: true
}, {
id: 'tiramisu-athletic',
url: 'wss://tiramisu.mycard.moe:8923',
address: '112.124.105.11',
port: 8911,
custom: false,
replay: true
});
} else {
this.servers.push({
id: 'mercury-us-1-athletic',
url: 'wss://mercury-us-1.mycard.moe:7923',
address: '104.237.154.173',
port: 7911,
custom: true,
replay: true
}, {
id: 'mercury-us-1',
url: 'wss://mercury-us-1.mycard.moe:7923',
address: '104.237.154.173',
port: 8911,
custom: false,
replay: true
});
}
}
refresh_replay_rooms() {
this.replay_rooms_show = this.replay_rooms.filter((room) => {
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;
});
}
async ngOnInit() {
let locale: string;
if (this.settingsService.getLocale().startsWith('zh')) {
locale = 'zh-CN';
} else {
locale = 'en-US';
}
this.windbot = (<YGOProData>this.app.data).windbot[locale];
this.system_conf = path.join(this.app.local!.path, 'system.conf');
await this.refresh();
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);
};
connection.onerror = (event: ErrorEvent) => {
console.log('error', server.id, event);
this.rooms = this.rooms.filter(room => room.server !== server);
};
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.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 = [];
});
remote.ipcMain.on('YGOPro', (e: any, type: string) => {
console.log('rrrrr');
this.request_match(type);
});
}
async refresh() {
this.decks = await this.get_decks();
let system_conf = await this.load_system_conf();
if (this.decks.includes(system_conf.lastdeck)) {
this.current_deck = system_conf.lastdeck;
} else {
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('https://mycard.moe/ygopro/api/user', {
search: {
username: this.loginService.user.username
}
})
.map((response) => response.json())
.toPromise();
this.points.emit(points);
} catch (error) {
console.log(error);
}
};
async get_decks(): Promise<string[]> {
try {
let files: string[] = await fs.readdir(path.join(this.app.local!.path, 'deck'));
return files.filter(file => path.extname(file) === '.ydk').map(file => path.basename(file, '.ydk'));
} catch (error) {
return [];
}
}
async get_replays(): Promise<string[]> {
try {
let files: string[] = await fs.readdir(path.join(this.app.local!.path, 'replay'));
return files.filter(file => path.extname(file) === '.yrp').map(file => path.basename(file, '.yrp'));
} catch (error) {
return [];
}
}
async get_font(files: string[]): Promise<string | undefined> {
for (let file of files) {
if (await fs.pathExists(file)) {
return file;
}
}
}
async delete_deck(deck: string) {
if (confirm('确认删除?')) {
try {
await fs.unlink(path.join(this.app.local!.path, 'deck', 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<SystemConf> {
let data = await fs.readFile(this.system_conf, { encoding: 'utf-8' });
return ini.parse(data);
};
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(['-j']);
};
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]);
}
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')]);
}
join_windbot(name?: string) {
if (!name) {
name = this.windbot[Math.floor(Math.random() * this.windbot.length)];
}
return this.join('AI#' + name, this.servers[0]);
}
async start_game(args: string[]) {
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((resolve, reject) => {
let child = child_process.spawn(path.join(this.app.local!.path, this.app.actions.get('main')!.execute), args, {
cwd: this.app.local!.path,
stdio: 'inherit'
});
child.on('error', (error) => {
reject(error);
win.restore();
});
child.on('exit', async (code, signal) => {
// error 触发之后还可能会触发exit,但是Promise只承认首次状态转移,因此这里无需重复判断是否已经error过。
await this.refresh();
resolve();
win.restore();
});
try {
this.http.get('https://mycard.moe/ygopro/api/history', {
search: {
page: 1,
username: this.loginService.user.username,
type: 0,
page_num: 1
}
})
.map((response) => response.json())
.toPromise()
.then((d) => {
start_time = d.data[0].start_time;
});
} catch (error) {
console.log(error);
}
try {
this.http.get('https://api.mycard.moe/ygopro/arena/user', { search: { username: this.loginService.user.username } })
.map((response) => response.json())
.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('https://mycard.moe/ygopro/api/history', {
search: {
page: 1,
username: this.loginService.user.username,
// username: "星光pokeboy",
type: 0,
page_num: 1
}
})
.map((response) => response.json())
.toPromise()
.then((d) => {
data = d.data[0];
data.myname = this.loginService.user.username;
});
await this.http.get('https://api.mycard.moe/ygopro/arena/user', {
search: {
username: this.loginService.user.username,
}
})
.map((response) => response.json())
.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('end_YGOPro_single.html', data, 202, 222);
}
});
} catch (error) {
console.log(error);
}
};
create_room(room: Room) {
let options_buffer = new Buffer(6);
// 建主密码 https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit
options_buffer.writeUInt8((room.private ? 2 : 1) << 4, 1);
options_buffer.writeUInt8(
room.options.rule << 5 |
room.options.mode << 3 |
(room.options.enable_priority ? 1 << 2 : 0) |
(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.servers[0]);
}
copy(text: string, event: Event) {
clipboard.writeText(text);
$('#copy-wrapper').tooltip({ trigger: 'manual' }).tooltip('show');
}
join_room(room: Room) {
let options_buffer = new Buffer(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 = new Buffer(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.servers[0]);
}
request_match(arena = 'entertain') {
let headers = new Headers();
headers.append('Authorization',
'Basic ' + Buffer.from(this.loginService.user.username + ':' + this.loginService.user.external_id).toString('base64'));
match_started_at = new Date();
this.matching_arena = matching_arena = arena;
this.matching = matching = this.http.post('https://api.mycard.moe/ygopro/match', null, {
headers: headers,
search: {
arena,
locale: this.settingsService.getLocale()
}
}).map(response => response.json())
.subscribe((data) => {
this.join(data['password'], { 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: any) {
// 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);
// }
}
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
'use strict';
Object.defineProperty(exports, '__esModule', {value: true});
const webpack = require('webpack');
const path = require('path');
const glob_copy_webpack_plugin_1 = require('../../plugins/glob-copy-webpack-plugin');
const named_lazy_chunks_webpack_plugin_1 = require('../../plugins/named-lazy-chunks-webpack-plugin');
const utils_1 = require('./utils');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const CircularDependencyPlugin = require('circular-dependency-plugin');
/**
* Enumerate loaders and their dependencies from this file to let the dependency validator
* know they are used.
*
* require('source-map-loader')
* require('raw-loader')
* require('script-loader')
* require('json-loader')
* require('url-loader')
* require('file-loader')
* require('@angular-devkit/build-optimizer')
*/
function getCommonConfig(wco) {
const {projectRoot, buildOptions, appConfig} = wco;
const appRoot = path.resolve(projectRoot, appConfig.root);
const nodeModules = path.resolve(projectRoot, 'node_modules');
let extraPlugins = [];
let extraRules = [];
let entryPoints = {};
if (appConfig.main) {
entryPoints['main'] = [path.resolve(appRoot, appConfig.main)];
}
if (appConfig.polyfills) {
entryPoints['polyfills'] = [path.resolve(appRoot, appConfig.polyfills)];
}
// determine hashing format
const hashFormat = utils_1.getOutputHashFormat(buildOptions.outputHashing);
// process global scripts
if (appConfig.scripts.length > 0) {
const globalScripts = utils_1.extraEntryParser(appConfig.scripts, appRoot, 'scripts');
// add entry points and lazy chunks
globalScripts.forEach(script => {
let scriptPath = `script-loader!${script.path}`;
entryPoints[script.entry] = (entryPoints[script.entry] || []).concat(scriptPath);
});
}
// process asset entries
if (appConfig.assets) {
extraPlugins.push(new glob_copy_webpack_plugin_1.GlobCopyWebpackPlugin({
patterns: appConfig.assets,
globOptions: {cwd: appRoot, dot: true, ignore: '**/.gitkeep'}
}));
}
if (buildOptions.progress) {
extraPlugins.push(new ProgressPlugin({profile: buildOptions.verbose, colors: true}));
}
if (buildOptions.showCircularDependencies) {
extraPlugins.push(new CircularDependencyPlugin({
exclude: /(\\|\/)node_modules(\\|\/)/
}));
}
if (buildOptions.buildOptimizer) {
extraRules.push({
test: /\.js$/,
use: [{
loader: '@angular-devkit/build-optimizer/webpack-loader',
options: {sourceMap: buildOptions.sourcemaps}
}]
});
}
if (buildOptions.namedChunks) {
extraPlugins.push(new named_lazy_chunks_webpack_plugin_1.NamedLazyChunksWebpackPlugin());
}
return {
target: 'electron-renderer',
resolve: {
extensions: ['.ts', '.js'],
modules: ['node_modules', nodeModules],
symlinks: !buildOptions.preserveSymlinks
},
resolveLoader: {
modules: [nodeModules, 'node_modules']
},
context: __dirname,
entry: entryPoints,
output: {
path: path.resolve(projectRoot, buildOptions.outputPath),
publicPath: buildOptions.deployUrl,
filename: `[name]${hashFormat.chunk}.bundle.js`,
chunkFilename: `[id]${hashFormat.chunk}.chunk.js`
},
module: {
rules: [
{enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules]},
{test: /\.json$/, loader: 'json-loader'},
{test: /\.html$/, loader: 'raw-loader'},
{test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]`},
{
test: /\.(jpg|png|webp|gif|otf|ttf|woff|woff2|cur|ani)$/,
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
},
{test: require.resolve('bootstrap'), use: 'imports-loader?jQuery=jquery,Tether=tether'},
{test: require.resolve('candy/libs.bundle.js'), use: 'imports-loader?jQuery=jquery'},
{test: require.resolve('candy/libs.bundle.js'), use: 'exports-loader?Mustache,Strophe,Base64,MD5'},
{
test: require.resolve('candy'),
use: 'imports-loader?jQuery=jquery,{Mustache%2CStrophe%2CBase64%2CMD5}=candy/libs.bundle.js'
},
{test: require.resolve('candy'), use: 'exports-loader?Candy'},
{test: require.resolve('candy-shop/me-does/candy.js'), use: 'imports-loader?jQuery=jquery,Candy=candy'},
{test: require.resolve('candy-shop/me-does/candy.js'), use: 'exports-loader?CandyShop.MeDoes'},
{test: require.resolve('candy-shop/modify-role/candy.js'), use: 'imports-loader?jQuery=jquery,Candy=candy'},
{test: require.resolve('candy-shop/modify-role/candy.js'), use: 'exports-loader?CandyShop.ModifyRole'},
{test: require.resolve('candy-shop/namecomplete/candy.js'), use: 'imports-loader?jQuery=jquery,Candy=candy'},
{test: require.resolve('candy-shop/namecomplete/candy.js'), use: 'exports-loader?CandyShop.NameComplete'},
{test: require.resolve('candy-shop/notifications/candy.js'), use: 'imports-loader?jQuery=jquery,Candy=candy'},
{test: require.resolve('candy-shop/notifications/candy.js'), use: 'exports-loader?CandyShop.Notifications'},
{test: require.resolve('candy-shop/notifyme/candy.js'), use: 'imports-loader?jQuery=jquery,Candy=candy'},
{test: require.resolve('candy-shop/notifyme/candy.js'), use: 'exports-loader?CandyShop.NotifyMe'},
{test: require.resolve('candy-shop/refocus/candy.js'), use: 'imports-loader?jQuery=jquery,Candy=candy'},
{test: require.resolve('candy-shop/refocus/candy.js'), use: 'exports-loader?CandyShop.Refocus'}
].concat(extraRules)
},
plugins: [
new webpack.NoEmitOnErrorsPlugin()
].concat(extraPlugins),
node: {
fs: 'empty',
// `global` should be kept true, removing it resulted in a
// massive size increase with Build Optimizer on AIO.
global: true,
crypto: 'empty',
tls: 'empty',
net: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: false
},
externals: {
bufferutil: "require('bufferutil')",
'utf-8-validate': "require('utf-8-validate')",
iconv: "require('iconv')",
'iconv-loader': "require('iconv')",
}
};
}
exports.getCommonConfig = getCommonConfig;
//# sourceMappingURL=/users/hansl/sources/angular-cli/models/webpack-configs/common.js.map
This diff was suppressed by a .gitattributes entry.
<!DOCTYPE html>
<html>
<head>
<title>MyCard</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="node_modules/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch((error) => {
document.getElementById('loading').setAttribute('hidden', 'hidden');
document.getElementById('failed').removeAttribute('hidden');
document.getElementById('error').removeAttribute('hidden');
document.getElementById('error').textContent = error;
});
</script>
</head>
<body>
<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 class="loading">
<img src="./images/CubbitLogo.png">
<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>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
</body>
</html>
'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(new Buffer(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 } = require('electron');
const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev');
const child_process = require('child_process');
const path = require('path');
// 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;
// 单实例
const shouldQuit = app.makeSingleInstance((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 (shouldQuit) {
app.quit();
}
// 调试模式
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);
});
autoUpdater.on('checking-for-update', () => {
global.update_status = 'checking-for-update';
console.log('autoUpdater', 'checking-for-update');
});
autoUpdater.on('update-available', () => {
global.update_status = 'update-available';
console.log('autoUpdater', 'update-available');
});
autoUpdater.on('update-not-available', () => {
global.update_status = 'update-not-available';
console.log('autoUpdater', '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();
});
});
// 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;
default:
throw 'unsupported platform';
}
return child_process.spawn(aria2c_path, ['--enable-rpc', '--rpc-allow-origin-all', '--continue', '--split=10', '--min-split-size=1M', '--max-connection-per-server=10', '--remove-control-file', '--allow-overwrite'], { stdio: 'ignore' });
}
const aria2c = createAria2c();
// 主窗口
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 640,
minWidth: 1024,
minHeight: 640,
frame: process.platform === 'darwin',
// transparent: process.platform != 'darwin',
titleBarStyle: process.platform === 'darwin' ? 'hidden' : undefined
});
// and load the index.html of the app.
mainWindow.loadURL(`file://${__dirname}/index.html`);
mainWindow.webContents.on('new-window', function (e, url) {
e.preventDefault();
shell.openExternal(url);
});
// Open the DevTools.
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.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
createWindow();
if (process.platform === 'win32') {
createTray();
}
if (process.env['NODE_ENV'] === 'production') {
autoUpdater.checkForUpdates();
}
});
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// 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. You can also put them in separate files and require them here.
app.on('quit', () => {
// windows 在非 detach 模式下会自动退出子进程
if (process.platform !== 'win32') {
aria2c.kill();
}
});
import * as child_process from 'child_process';
import {ChildProcess} from 'child_process';
import {app, BrowserWindow, Menu, shell, Tray} from 'electron';
import * as isDev from 'electron-is-dev';
import * as Store from 'electron-store';
import {autoUpdater} from 'electron-updater';
import * as net from 'net';
import {Socket} from 'net';
import {EOL} from 'os';
import * as path from 'path';
import * as readline from 'readline';
import {argv} from 'yargs';
class Main {
static aria2: ChildProcess;
static mainWindow: Electron.BrowserWindow | null;
static updateWindow: Electron.BrowserWindow | null;
static tray: Electron.Tray;
static store = new Store();
// 提权
static handleElevate() {
if (argv['e']) {
if (process.platform === 'darwin') {
app.dock.hide();
}
const elevate = JSON.parse(new Buffer(argv['e'], 'base64').toString());
net.connect(elevate['ipc'], function (this: Socket) {
process.send = (message, sendHandle) => this.write(JSON.stringify(message) + EOL);
this.on('end', () => process.emit('disconnect', () => null));
readline.createInterface({input: this}).on('line', (line) => process.emit('message', JSON.parse(line)));
process.argv = elevate['arguments'][1];
require('./' + elevate['arguments'][0]);
});
return true;
}
}
static handleSingleInstance() {
// Someone tried to run a second instance, we should focus our window.
if (this.mainWindow) {
if (this.mainWindow.isMinimized()) {
this.mainWindow.restore();
}
if (!this.mainWindow.isVisible()) {
this.mainWindow.show();
}
this.mainWindow.focus();
}
}
static createAria2() {
const aria2c = path.join(process.env['NODE_ENV'] === 'production' ? process.resourcesPath! : app.getAppPath(),
'bin', process.platform === 'win32' ? 'aria2c.exe' : 'aria2c');
return child_process.spawn(aria2c, [
'--enable-rpc',
'--rpc-allow-origin-all',
'--continue',
'--split=10',
'--min-split-size=1M',
'--max-connection-per-server=10',
'--remove-control-file',
'--allow-overwrite'
], {stdio: 'ignore'});
}
static createWindow() {
// Create the browser window.
this.mainWindow = new BrowserWindow({
width: 1024,
height: 640,
minWidth: 1024,
minHeight: 640,
frame: process.platform === 'darwin',
titleBarStyle: process.platform === 'darwin' ? 'hidden' : undefined,
webPreferences: {
webSecurity: false
}
});
let locale = this.store.get('locale') || app.getLocale();
locale = locale.startsWith('zh') ? 'zh-CN' : 'en-US';
// and load the index.html of the app.
this.mainWindow.loadURL(argv.url || `file://${__dirname}/index.html`);
// Open the DevTools.
if (isDev) {
this.mainWindow.webContents.openDevTools();
}
// 在浏览器中打开新窗口
this.mainWindow.webContents.on('new-window', function (event, url) {
event.preventDefault();
shell.openExternal(url);
});
// Emitted when the window is closed.
this.mainWindow.on('closed', () => {
// 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.
this.mainWindow = null;
});
}
static createTray() {
const icon = path.join(process.env['NODE_ENV'] === 'production' ? process.resourcesPath! : app.getAppPath(), 'assets', 'icon.ico');
this.tray = new Tray(icon);
this.tray.setToolTip('MoeCube');
this.tray.setContextMenu(Menu.buildFromTemplate([
{label: '显示主界面', type: 'normal', click: this.toggleMainWindow},
{label: '退出', type: 'normal', click: app.quit}
]));
}
static toggleMainWindow() {
if (this.mainWindow!.isVisible()) {
this.mainWindow!.hide();
} else {
this.mainWindow!.show();
}
}
static main() {
if (this.handleElevate()) {
return;
}
// 单例
if (app.makeSingleInstance(this.handleSingleInstance)) {
return;
}
// 调试模式
if (!process.env['NODE_ENV']) {
process.env['NODE_ENV'] = isDev ? 'development' : 'production';
}
this.aria2 = this.createAria2();
global['autoUpdater'] = autoUpdater;
autoUpdater.on('update-downloaded', () => {
this.updateWindow = new BrowserWindow({width: 640, height: 360});
this.updateWindow.loadURL(`file://${__dirname}/update/index.html`);
this.updateWindow.on('closed', () => this.updateWindow = null);
});
app.on('ready', () => {
this.createWindow();
if (process.platform === 'win32') {
this.createTray();
}
if (process.env['NODE_ENV'] === 'production') {
autoUpdater.checkForUpdates();
}
});
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS 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', () => {
// 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 (!this.mainWindow) {
this.createWindow();
}
});
app.on('quit', () => {
// windows 在非 detach 模式下会自动退出子进程
if (process.platform !== 'win32') {
this.aria2.kill();
}
});
}
}
Main.main();
This source diff could not be displayed because it is too large. You can view the blob instead.
const raw = require("raw-socket"); import raw from 'raw-socket';
let socket = raw.createSocket({protocol: raw.Protocol.UDP});
let connect = (local_port, remote_port, remote_address)=> { const socket = raw.createSocket({ protocol: raw.Protocol.UDP });
let buffer = new Buffer(9);
class Handler {
static connect(local_port, remote_port, remote_address) {
const buffer = new Buffer(9);
buffer.writeUInt16BE(local_port, 0); buffer.writeUInt16BE(local_port, 0);
buffer.writeUInt16BE(remote_port, 2); buffer.writeUInt16BE(remote_port, 2);
buffer.writeUInt16BE(buffer.length, 4); buffer.writeUInt16BE(buffer.length, 4);
socket.send(buffer, 0, buffer.length, remote_address, (error, bytes) => { socket.send(buffer, 0, buffer.length, remote_address, (error, bytes) => {
if (error) { });
throw(error); };
} }
})
};
let handler = {connect: connect};
process.on('message', (message)=> { process.on('message', (message) => Handler[message.method](...message.params));
handler[message.action](...message.arguments);
});
process.on('disconnect', process.exit); process.on('disconnect', process.exit);
process.send('initialized'); process.send!('initialized');
\ No newline at end of file
document.addEventListener("DOMContentLoaded", function(event) {
$('#nav,.navbar-header').removeClass('nav-xs');
});
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"name": "mycard", "name": "mycard",
"version": "3.0.36", "version": "3.1.0",
"description": "moecube", "description": "moecube",
"keywords": [], "keywords": [],
"author": "zh99998 <zh99998@gmail.com>", "author": "zh99998 <zh99998@gmail.com>",
...@@ -8,80 +8,86 @@ ...@@ -8,80 +8,86 @@
"license": "UNLICENSED", "license": "UNLICENSED",
"repository": "moecube/moecube", "repository": "moecube/moecube",
"scripts": { "scripts": {
"start": "tsc && electron .", "start": "electron . --url=http://localhost:4200",
"lint": "tslint ./app/*.ts -t verbose", "start:aot": "electron .",
"pack": "tsc && build --dir", "pack": "npm run build && build --dir",
"dist": "tsc && build", "dist": "npm run build && build",
"release": "tsc && build", "dev": "ng serve --extract-css --sourcemaps",
"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js", "dev:zh-CN": "ng serve --extract-css --sourcemaps --aot",
"tsc": "tsc", "dev:en-US": "ng serve --extract-css --sourcemaps --aot --locale en-US --i18n-format xlf --i18n-file src/locale/messages.en-US.xlf",
"i18n": "ng-xi18n --i18nFormat 'xlf2' && sed -i.bak 's/source-language=\"en\"/source-language=\"zh-CN\"/' messages.xlf", "build": "npm run build:zh-CN && npm run build:en-US",
"build:zh-CN": "ng build --extract-css --env=prod --aot --output-path public/zh-CN",
"build:en-US": "ng build --extract-css --env=prod --aot --output-path public/en-US --locale en-US --i18n-format xlf --i18n-file src/locale/messages.en-US.xlf",
"lint": "ng lint",
"i18n": "ng xi18n --locale=zh-CN",
"i18n:upload": "npm run i18n && curl --location --user ${TRANSIFEX_USERNAME}:${TRANSIFEX_PASSWORD} --request PUT --header 'Content-type: multipart/form-data' --form content=@messages.xlf https://www.transifex.com/api/2/project/moecube/resource/messages/content/", "i18n:upload": "npm run i18n && curl --location --user ${TRANSIFEX_USERNAME}:${TRANSIFEX_PASSWORD} --request PUT --header 'Content-type: multipart/form-data' --form content=@messages.xlf https://www.transifex.com/api/2/project/moecube/resource/messages/content/",
"i18n:translate": "open https://www.transifex.com/moecube/moecube/translate/#en_US/messages/108367258?translated=no", "i18n:translate": "open https://www.transifex.com/moecube/moecube/translate/#en_US/messages/108367258?translated=no",
"i18n:download": "curl --output locale/messages.en-US.xlf --retry 5 --location --user ${TRANSIFEX_USERNAME}:${TRANSIFEX_PASSWORD} https://www.transifex.com/api/2/project/moecube/resource/messages/translation/en_US/?file && sed -i.bak 's/\\&amp;/\\&/g; s/\\&lt;/</g; s/\\&gt;/>/g; s/\\&quot;/\"/g;' locale/messages.en-US.xlf" "i18n:download": "curl --output src/locale/messages.en-US.xlf --retry 5 --location --user ${TRANSIFEX_USERNAME}:${TRANSIFEX_PASSWORD} https://www.transifex.com/api/2/project/moecube/resource/messages/translation/en_US/?file && sed -i.bak 's/\\&amp;/\\&/g; s/\\&lt;/</g; s/\\&gt;/>/g; s/\\&quot;/\"/g;' src/locale/messages.en-US.xlf",
"electron-rebuild": "electron-rebuild",
"postinstall": "cp common.js node_modules/@angular/cli/models/webpack-configs/common.js"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "latest", "@angular/animations": "^4.2.4",
"@angular/common": "latest", "@angular/common": "^4.2.4",
"@angular/compiler": "latest", "@angular/compiler": "^4.2.4",
"@angular/core": "latest", "@angular/core": "^4.2.4",
"@angular/forms": "latest", "@angular/forms": "^4.2.4",
"@angular/http": "latest", "@angular/http": "^4.2.4",
"@angular/platform-browser": "latest", "@angular/platform-browser": "^4.2.4",
"@angular/platform-browser-dynamic": "latest", "@angular/platform-browser-dynamic": "^4.2.4",
"@angular/router": "latest", "@angular/router": "^4.2.4",
"@types/fs-extra": "^3.0.3", "aria2": "^3.0.0",
"@types/uuid": "^3.0.0", "bootstrap": "^4.0.0-alpha.6",
"angular-in-memory-web-api": "latest",
"aria2": "latest",
"bootstrap": "next",
"candy": "https://github.com/moecube/candy/releases/download/v2.2.0/candy.tar.gz", "candy": "https://github.com/moecube/candy/releases/download/v2.2.0/candy.tar.gz",
"candy-shop": "candy-chat/candy-plugins", "candy-shop": "zh99998/candy-plugins#patch-5",
"core-js": "latest", "core-js": "^2.4.1",
"electron-is-dev": "latest", "electron-cookies": "^1.1.0",
"electron-sudo": "moecube/electron-sudo#moecube", "electron-is-dev": "^0.3.0",
"electron-updater": "latest", "electron-schema": "moecube/electron-schema",
"font-awesome": "latest", "electron-store": "^1.2.0",
"fs-extra": "^3.0.1", "electron-sudo": "github:moecube/electron-sudo#moecube",
"glob": "latest", "electron-updater": "^2.8.0",
"ini": "latest", "exports-loader": "^0.6.4",
"jquery": "^2.2.4", "font-awesome": "^4.7.0",
"marked": "latest", "glob": "^7.1.2",
"raven-js": "latest", "i18n": "^0.8.3",
"raw-socket": "latest", "imports-loader": "^0.7.1",
"reconnecting-websocket": "latest", "ini": "^1.3.4",
"reflect-metadata": "latest", "jquery": "^3.2.1",
"rxjs": "latest", "lodash": "^4.17.4",
"systemjs": "latest", "marked": "^0.3.6",
"systemjs-plugin-text": "latest", "ng2-split-pane": "^1.4.0",
"tether": "latest", "raw-socket": "^1.5.1",
"typeahead.js": "latest", "reconnecting-websocket": "^3.0.7",
"uuid": "^3.0.1", "rxjs": "^5.4.2",
"vue": "latest", "tether": "^1.4.0",
"zone.js": "latest" "typeahead.js": "^0.11.1",
"uuid": "^3.1.0",
"vue": "^2.4.2",
"yargs": "^8.0.2",
"zone.js": "^0.8.14"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "latest", "@angular/cli": "1.3.0-rc.3",
"@angular/platform-server": "latest", "@angular/compiler-cli": "^4.2.4",
"@types/bootstrap": "latest", "@angular/language-service": "^4.2.4",
"@types/glob": "latest", "@types/bootstrap": "^3.3.35",
"@types/ini": "latest", "@types/glob": "^5.0.30",
"@types/jquery": "^2.0.47", "@types/ini": "^1.3.29",
"@types/marked": "latest", "@types/jquery": "^3.2.10",
"@types/node": "latest", "@types/lodash": "^4.14.71",
"@types/raven-js": "latest", "@types/marked": "0.0.28",
"@types/tether": "latest", "@types/node": "^6.0.85",
"@types/typeahead": "latest", "@types/tether": "^1.4.2",
"electron": "1.4.15", "@types/typeahead": "^0.11.29",
"@types/electron": "1.4.23", "@types/uuid": "^3.4.0",
"electron-builder": "latest", "codelyzer": "~3.1.1",
"electron-rebuild": "latest", "copy-webpack-plugin": "^4.0.1",
"rollup": "latest", "electron": "^1.6.11",
"rollup-plugin-commonjs": "latest", "electron-builder": "^19.17.0",
"rollup-plugin-node-resolve": "latest", "electron-rebuild": "^1.6.0",
"rollup-plugin-uglify": "latest", "tslint": "~5.3.2",
"tslint": "^3.15.1", "typescript": "~2.3.3"
"typescript": "latest"
}, },
"build": { "build": {
"productName": "MyCard", "productName": "MyCard",
...@@ -95,6 +101,16 @@ ...@@ -95,6 +101,16 @@
"provider": "github" "provider": "github"
} }
], ],
"files": [
"app",
"index.js",
"package.json",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}",
"!**/node_modules/.bin",
"!**/*.{o,hprof,orig,pyc,pyo,rbc}",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes,.editorconfig,.flowconfig,.yarn-metadata.json,.idea,appveyor.yml,.travis.yml,circle.yml,npm-debug.log,.nyc_output,yarn.lock,.yarn-integrity}"
],
"extraResources": [ "extraResources": [
"bin" "bin"
], ],
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="./node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" /> <link href="../node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<style> <style>
html,div,body,td,tr{ html,div,body,td,tr{
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="./node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" /> <link href="../node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<style> <style>
html,div,body,td,tr{ html,div,body,td,tr{
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="./node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" /> <link href="../node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<style> <style>
@font-face { @font-face {
font-family: 'Mogra'; font-family: 'Mogra';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('font/Mogra-Regular.ttf'); src: url('../font/Mogra-Regular.ttf');
} }
html,div,body,td,tr{ html,div,body,td,tr{
background: rgba(0,0,0,0); background: rgba(0,0,0,0);
......
import rollup from 'rollup'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs';
import uglify from 'rollup-plugin-uglify'
//paths are relative to the execution path
export default {
entry: 'app/main-aot.js',
dest: 'aot/dist/build.js', // output a single application bundle
sourceMap: true,
sourceMapFile: 'aot/dist/build.js.map',
format: 'iife',
plugins: [
nodeResolve({jsnext: true, module: true}),
commonjs({
include: ['node_modules/rxjs/**']
}),
uglify()
]
}
:host {
display: block;
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;
}
#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-toggle="modal" data-target="#purchase-modal">{{currentApp.price.cny | currency:'CNY':true}} 购买</button>
<button i18n type="button" (click)="updateInstallOption(currentApp)" class="btn btn-secondary btn-sm" data-toggle="modal" data-target="#install-modal">安装试玩版</button>
<!--<button i18n (click)="updateInstallOption(currentApp)" type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#install-modal">我已经购买过</button>-->
</div>
<!--应用已购买,未安装-->
<div *ngIf="currentApp.isBought() && !currentApp.isInstalled()" class="i-b">
<button i18n (click)="updateInstallOption(currentApp)" type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#install-modal">安装</button>
<button i18n *ngIf="currentApp.runnable()" (click)="updateInstallOption(currentApp)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-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.id != 'ygopro')" 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 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-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>-->
<mycard-network *ngIf="(currentApp.id != 'ygopro') && currentApp && currentApp.network && currentApp.network.protocol == 'maotama'" [currentApp]="currentApp"></mycard-network>
<mycard-ygopro *ngIf="currentApp.isReady() && (currentApp.id == 'ygopro')" [app]="currentApp" [currentApp]="currentApp" (points)="onPoints($event)"></mycard-ygopro>
</div>
</div>
<div id="arena" class="panel panel-default" *ngIf="currentApp.id === 'ygopro' && points ">
<h2 i18n>排位成绩</h2>
<table class="table table-sm">
<tbody>
<tr>
<th i18n>竞技排名</th>
<td>{{points.arena_rank}}</td>
<th i18n>娱乐排名</th>
<td>{{points.exp_rank}}</td>
</tr>
<tr>
<th i18n>竞技胜率</th>
<td>{{points.athletic_wl_ratio}}%</td>
<th i18n>经验</th>
<td>{{points.exp}}</td>
</tr>
<tr>
<th i18n>胜场</th>
<td>{{points.athletic_win}}</td>
<th i18n>胜场</th>
<td>{{points.entertain_win}}</td>
</tr>
<tr>
<th i18n>负场</th>
<td>{{points.athletic_lose}}</td>
<th i18n>负场</th>
<td>{{points.entertain_lose}}</td>
</tr>
<tr>
<th i18n>平局</th>
<td>{{points.athletic_draw}}</td>
<th i18n>平局</th>
<td>{{points.entertain_draw}}</td>
</tr>
<tr>
<th i18n>总场</th>
<td>{{points.athletic_all}}</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-danger 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>
</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 *ngIf="mods && mods.length">
<table class="table table-striped">
<thead class="thead-inverse">
<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>{{mod.name}}</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" *ngIf="installOption">
<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="close" data-dismiss="modal" aria-label="Close">
<span>&times;</span>
</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 == '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-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" *ngIf="installOption">
<div class="modal-dialog" role="document">
<form id="import-form" class="modal-content" (ngSubmit)="importGame(currentApp,installOption,referencesInstall)" #theForm="ngForm">
<div class="modal-header">
<h5 i18n class="modal-title">导入 {{currentApp.name}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p i18n>选择主程序 {{currentApp.actions.get('main').execute}}</p>
<label class="custom-file" lang="en">
<input (click)="$event.preventDefault();selectImport(currentApp)" type="file" id="file" class="custom-file-input">
<span class="custom-file-control">{{import_path || currentApp.actions.get('main').execute}}</span>
</label>
<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-dismiss="modal">取消</button>
<button i18n type="submit" [disabled]="import_path && !theForm.form.valid" 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="close" data-dismiss="modal" aria-label="Close">
<span>&times;</span>
</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-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="close" data-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-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 '../install-option';
import {SettingsService} from '../settings.service';
import {App} from '../app';
import {DownloadService} from '../download.service';
import {clipboard, remote} from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import * as $ from 'jquery';
import {Points} from '../ygopro/ygopro.component';
import {Http} from '@angular/http';
import {LoginService} from '../login.service';
declare const Notification: any;
// declare interface Window {
// adsbygoogle: any[];
// }
//
// declare var adsbygoogle: any[];
@Component({
selector: 'mycard-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: Points;
tags: {};
payment = 'alipay';
creating_order = false;
constructor(private appsService: AppsService, private settingsService: SettingsService,
private downloadService: DownloadService, private ref: ChangeDetectorRef, private el: ElementRef,
private http: Http, private loginService: LoginService) {
this.tags = this.settingsService.getLocale().startsWith('zh') ? {
'recommend': '推荐',
'mysterious': '迷之物体',
'touhou': '东方 Project',
'touhou_pc98': '东方旧作',
'language': '语言包'
} : {
'recommend': 'Recommended',
'mysterious': 'Something',
'touhou': 'Touhou Project',
'touhou_pc98': 'Touhou old series',
'language': 'Language Pack'
};
}
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';
}
// 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();
});
});
}
}
updateInstallOption(app: App) {
this.installOption = new InstallOption(app);
this.installOption.installLibrary = this.settingsService.getDefaultLibrary().path;
this.references = Array.from(app.references.values());
console.log(this.references);
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: string) {
this.appsService.runApp(app, action_name);
}
custom(app: App) {
this.appsService.runApp(app, 'custom');
}
async importGame(targetApp: App, option: InstallOption, referencesInstall: { [id: string]: boolean }) {
$('#import-modal').modal('hide');
let dir = path.dirname(this.import_path);
// 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: Points) {
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://api.mycard.moe/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;
}
}
...@@ -11,16 +11,16 @@ export class AppLocal { ...@@ -11,16 +11,16 @@ export class AppLocal {
update(local: any) { update(local: any) {
this.path = local.path; this.path = local.path;
this.version = local.version; this.version = local.version;
let files = new Map<string, string>(); const files = new Map<string, string>();
for (let filename of Object.keys(local.files)) { for (const filename of Object.keys(local.files)) {
files.set(filename, local.files[filename]); files.set(filename, local.files[filename]);
} }
this.files = files; this.files = files;
} }
toJSON() { toJSON() {
let t: any = {}; const t: any = {};
for (let [k, v] of this.files) { for (const [k, v] of this.files) {
t[k] = v; t[k] = v;
} }
return {path: this.path, version: this.version, files: t}; return {path: this.path, version: this.version, files: t};
......
import {ApplicationRef, EventEmitter, Injectable, NgZone} from '@angular/core';
import {Http} from '@angular/http';
import * as child_process from 'child_process';
import {ChildProcess} from 'child_process';
import * as crypto from 'crypto';
import {remote} from 'electron';
import * as sudo from 'electron-sudo';
import * as fs from 'fs';
import * as glob from 'glob';
import * as ini from 'ini';
import * as path from 'path';
import * as readline from 'readline';
import {Observable, Observer} from 'rxjs/Rx';
import {Action, App, AppStatus} from './app';
import {AppLocal} from './app-local';
import {DownloadService, DownloadStatus} from './download.service';
import {InstallOption} from './install-option';
import {LoginService} from './login.service';
import {SettingsService} from './settings.service';
import {ComparableSet} from './shared/ComparableSet';
import ReadableStream = NodeJS.ReadableStream;
import Timer = NodeJS.Timer;
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()
export class AppsService {
eventEmitter = new EventEmitter<void>();
map: Map<string, string> = new Map();
connections = new Map<App, Connection>();
maotama: Promise<ChildProcess>;
readonly tarPath = process.platform === 'win32' ?
path.join(process.env['NODE_ENV'] === 'production' ? process.resourcesPath! : '', 'bin', 'bsdtar.exe')
: 'bsdtar';
loadAppsList = (data: any): Map<string, App> => {
const apps = new Map<string, App>();
const locale = this.settingsService.getLocale();
const platform = process.platform;
for (const item of data) {
const app = new App(item);
const 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 (const 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 (const key of ['actions', 'dependencies', 'references', 'version']) {
if (app[key]) {
if (app[key][platform]) {
app[key] = app[key][platform];
} else {
app[key] = null;
}
}
}
// 时间
if (app.released_at) {
app.released_at = new Date(app.released_at);
}
if (app.news) {
for (const _item of app.news) {
_item.updated_at = new Date(_item.updated_at);
}
}
apps.set(item.id, app);
}
// 设置App关系
for (const app of apps.values()) {
const temp = app.actions;
const map = new Map<string, any>();
for (const action of Object.keys(temp)) {
const openId = temp[action]['open'];
if (openId) {
temp[action]['open'] = apps.get(openId);
}
map.set(action, temp[action]);
}
app.actions = map;
for (const key of ['dependencies', 'references', 'parent']) {
const value = app[key];
if (value) {
if (Array.isArray(value)) {
const _map = new Map<string, App>();
for (const 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;
};
private apps: Map<string, App>;
constructor(private http: Http, private settingsService: SettingsService, private ref: ApplicationRef,
private downloadService: DownloadService, private ngZone: NgZone, private loginService: LoginService) {
}
get lastVisited(): App | undefined {
const 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() {
const appsURL = 'https://api.mycard.moe/apps.json';
const keysURL = 'https://api.mycard.moe/keys';
try {
const data = await this.http.get(appsURL).map((response) => response.json()).toPromise();
const keys_data = await this.http.get(keysURL, {
search: {
user_id: this.loginService.user.email
}
}).map((response) => response.json()).toPromise();
for (const item of keys_data) {
const 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);
const data = localStorage.getItem('apps_json');
if (data) {
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
const 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') {
const volume = legacy_ygopro_path.split(':')[0].toUpperCase();
for (const _library of this.settingsService.getLibraries()) {
if (_library.path.split(':')[0].toUpperCase() === volume) {
library = _library.path;
}
}
if (!library) {
try {
const _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;
}
const 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() {
const libraries = this.settingsService.getLibraries();
for (const 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));
}
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) => {
const readable = fs.createReadStream(src);
readable.on('open', () => {
const 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';
const checksumFiles = await this.getChecksumFile(app);
for (const [pattern, fileOption] of app.files) {
await new Promise((resolve, reject) => {
return new glob.Glob(pattern, {cwd: appPath}, (err, files) => {
for (const file of files) {
// 避免被当做文件夹
if (fileOption.sync) {
checksumFiles.set(file, 'DO_NOT_CARE_HASH');
}
}
resolve();
});
});
}
await this.createDirectory(option.installDir);
const 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;
// 刷新进度
const interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => {
this.ngZone.runOutsideAngular(async () => {
try {
for (const [file, checksum] of sortedFiles) {
const src = path.join(appPath, file);
const 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();
} 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) => {
const 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', () => {
const 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>> {
const result = new Map<string, string>();
for (const [file, checksum] of checksumFiles) {
const filePath = path.join(app.local!.path, file);
// 如果文件不存在,随便生成一个checksum
await new Promise((resolve, reject) => {
fs.access(filePath, fs.constants.F_OK, async (err: Error) => {
if (err) {
result.set(file, Math.random().toString());
} else if (checksum === '') {
result.set(file, '');
} else {
const sha256sum = await this.sha256sum(filePath);
result.set(file, sha256sum);
}
callback();
resolve();
});
});
}
return result;
}
async update(app: App, verify = false) {
let readyToUpdate = false;
// 已经安装的mod
const 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);
const latestFiles = await this.getChecksumFile(app);
let localFiles: Map<string, string> | undefined;
if (verify) {
// 刷新进度条
const 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();
} catch (e) {
reject(e);
}
});
});
clearInterval(interval);
} else {
localFiles = app.local!.files;
}
const addedFiles: Set<string> = new Set<string>();
const changedFiles: Set<string> = new Set<string>();
const deletedFiles: Set<string> = new Set<string>();
// 遍历寻找新增加的文件
for (const [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));
}
}
const ignoreFiles: Set<string> = new Set();
for (const [pattern, fileOption] of app.files) {
await new Promise((resolve, reject) => {
return new glob.Glob(pattern, {cwd: app.local!.path}, (err, files) => {
for (const file of files) {
if (fileOption.ignore) {
ignoreFiles.add(file);
}
}
resolve();
});
});
}
// 遍历寻找旧版本与新版本不一样的文件和新版本比旧版少了的文件
// ignoreFiles里的文件不作处理
for (const [file, checksum] of localFiles!) {
if (latestFiles.has(file)) {
const 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);
}
}
const backupFiles: string[] = [];
const restoreFiles: string[] = [];
if (app.parent) {
const parentFiles = app.parent.local!.files;
// 新增加的文件和parent冲突,且不是目录,就添加backup到
// 改变的文件不做备份
for (const addedFile of addedFiles) {
if (parentFiles.has(addedFile) && parentFiles.get(addedFile) !== '') {
backupFiles.push(addedFile);
}
}
// 如果要删除的文件parent里也有就恢复这个文件
for (const deletedFile of deletedFiles) {
restoreFiles.push(deletedFile);
}
const 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 (const mod of mods) {
// 更新时,冲突文件在backup目录里,需要更新backup目录里的文件
// 如果changed列表与已经安装的mod有冲突,就push到backup列表里
// 然后先把当前的mod文件被分到mods_backup目录再解压更新,把文件备份到backup,最后从mods_backup里恢复mods文件
// 校验时,认为mod的文件正确,把冲突文件从changed列表里面删除掉
for (const changedFile of changedFiles) {
if (mod.local!.files.has(changedFile)) {
if (!verify) {
backupFiles.push(changedFile);
} else {
changedFiles.delete(changedFile);
}
}
}
const backupToDelete: string[] = [];
// 如果要删除的文件,mod里面存在,就删除backup目录里的文件
for (const deletedFile of deletedFiles) {
if (mod.local!.files.has(deletedFile)) {
backupToDelete.push(deletedFile);
}
}
const backupDir = path.join(path.dirname(app.local!.path), 'mods_backup', app.id);
await this.backupFiles(app.local!.path, backupDir, backupFiles);
for (const 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...');
const modsBackupDir = path.join(path.dirname(app.local!.path), 'mods_backup', app.id);
const 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';
}
const updateUrl = App.updateUrl(app, process.platform, locale);
const metalink = await this.http.post(updateUrl, changedFiles).map((response) => response.text()).toPromise();
const downloadDir = path.join(path.dirname(app.local!.path), 'downloading');
const 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();
});
const downloadFiles = await this.downloadService.getFiles(downloadId);
app.status.total = 0;
// 刷新进度条
const interval = setInterval(() => {
}, 500);
for (const downloadFile of downloadFiles) {
await new Promise((resolve, reject) => {
this.extract(downloadFile, app.local!.path).subscribe((file) => {
app.status.progressMessage = file;
}, (error) => {
reject(error);
}, () => {
resolve();
});
});
}
clearInterval(interval);
}
if (deletedFiles && deletedFiles.size > 0) {
Logger.info('Found files deleted: ', deletedFiles);
for (const 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();
} 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';
}
const metalinkUrl = App.downloadUrl(_app, process.platform, locale);
_app.status.status = 'downloading';
const metalink = await this.http.get(metalinkUrl).map((response) => response.text()).toPromise();
const 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;
}
const files = await this.downloadService.getFiles(downloadId);
_app.status.status = 'waiting';
return {app: _app, files: files};
};
if (!app.isInstalled()) {
const apps: App[] = [];
const dependencies = app.findDependencies().filter((dependency) => {
return !dependency.isInstalled();
});
apps.push(...dependencies, app);
try {
const downloadPath = path.join(option.installLibrary, 'downloading');
const tasks: Promise<any>[] = [];
Logger.info('Start to Download', apps);
for (const a of apps) {
tasks.push(addDownloadTask(a, downloadPath));
}
const downloadResults = await Promise.all(tasks);
Logger.info('Download Complete', downloadResults);
const installTasks: Promise<void>[] = [];
for (const result of downloadResults) {
const o = new InstallOption(result.app, option.installLibrary);
o.downloadFiles = result.files;
const task = tryToInstall({app: result.app, option: o});
installTasks.push(task);
}
await Promise.all(installTasks);
} catch (e) {
for (const a of apps) {
if (!a.isReady()) {
a.reset();
}
}
throw e;
}
}
}
findChildren(app: App): App[] {
const children: App[] = [];
for (const 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') {
const children = this.findChildren(app);
let cwd = (<AppLocal>app.local).path;
let action: Action = <Action>app.actions.get(action_name);
let args: string[] = [];
let env = {};
for (const child of children) {
if (child.isInstalled()) {
const _action = child.actions.get(action_name);
if (_action) {
action = _action;
}
}
}
let execute = path.join(cwd, action.execute);
if (app.id === 'th123') {
const th105 = <App>app.references.get('th105');
if (th105.isInstalled()) {
const config_file = path.join((<AppLocal>app.local).path, 'configex123.ini');
const config = await new Promise((resolve, reject) => {
fs.readFile(config_file, {encoding: 'utf-8'}, (error, data) => {
if (error) {
return reject(error);
}
resolve(ini.parse(data));
});
});
config['th105path'] = {path: (<AppLocal>th105.local).path};
await new Promise((resolve, reject) => {
fs.writeFile(config_file, ini.stringify(config), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
}
if (action.open) {
const np2 = action.open;
let openAction: Action;
openAction = np2.actions.get('main')!;
let openPath = np2.local!.path;
if (action.open.id === 'np2fmgen') {
const config_file = path.join(action.open!.local!.path, 'np21nt.ini');
const config = await new Promise((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:', app.local!.path, action.execute);
config['NekoProject21']['fontfile'] =
path.win32.join(process.platform === 'win32' ? '' : 'Z:', app.local!.path, 'font.bmp');
await new Promise((resolve, reject) => {
fs.writeFile(config_file, ini.stringify(config), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
if (process.platform !== 'win32') {
args.push(openAction.execute);
args = args.concat(openAction.args);
const wine = openAction.open!;
openPath = wine.local!.path;
openAction = openAction!.open!.actions.get('main')!;
}
cwd = np2.local!.path;
}
args = args.concat(openAction.args);
args.push(action.execute);
execute = path.join(openPath, openAction.execute);
env = Object.assign(env, openAction.env);
}
args = args.concat(action.args);
env = Object.assign(env, action.env);
console.log(execute, args, env, cwd);
const handle = child_process.spawn(execute, args, {env: env, cwd: cwd});
handle.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
handle.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
handle.on('close', (code) => {
console.log(`child process exited with code ${code}`);
remote.getCurrentWindow().restore();
});
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) => {
const child = sudo.fork('maotama', [], <any>{stdio: ['inherit', 'inherit', 'inherit', 'ipc']});
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);
const [action, args] = event.data.split(' ', 2);
const [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) {
const app = task.app;
if (!app.isWaiting()) {
console.error('doUninstall', '应用不处于等待安装状态', app);
throw new Error(('应用不处于等待安装状态'));
}
if (!app.readyForInstall()) {
console.error('doInstall', '应用依赖不齐备', app);
throw new Error(('应用依赖不齐备'));
}
try {
const option = task.option;
let installDir = option.installDir;
const checksumFile = await this.getChecksumFile(app);
const allFiles = new Set(checksumFile.keys());
app.status.status = 'installing';
app.status.total = allFiles.size;
app.status.progress = 0;
const interval = setInterval(() => {
}, 500);
if (app.parent) {
// mod需要安装到parent路径
installDir = app.parent.local!.path;
const parentFiles = new ComparableSet(Array.from(app.parent.local!.files.keys()));
const appFiles = new ComparableSet(Array.from(checksumFile.keys()));
const conflictFiles = appFiles.intersection(parentFiles);
app.status.total += conflictFiles.size;
if (conflictFiles.size > 0) {
const backupPath = path.join(option.installLibrary, 'backup', app.parent.id);
// 文件夹不需要备份,删除
for (const 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();
} catch (e) {
reject(e);
}
});
});
}
}
// let timeNow = new Date().getTime();
for (const 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();
});
});
}
clearInterval(interval);
await this.postInstall(app, installDir);
console.log('post install success');
const 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 new Error(`#{dir} exists and is not a directory`);
}
}
extract(file: string, dir: string): Observable<string> {
return Observable.create((observer: Observer<string>) => {
Logger.info('Start to extract... Command Line: ' + this.tarPath, file, dir);
const tarProcess = child_process.spawn(this.tarPath, ['xvf', file, '-C', dir]);
const rl = readline.createInterface({
input: <ReadableStream>tarProcess.stderr
});
rl.on('line', (input: string) => {
observer.next(input.split(' ', 2)[1]);
});
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) {
const action = app.actions.get('install');
if (action) {
let env = Object.assign({}, action.env);
const command: string[] = [];
command.push(action.execute);
command.push(...action.args);
const open = action.open;
if (open) {
const 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) => {
const 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 (const file of files) {
await new Promise(async (resolve, reject) => {
const srcPath = path.join(dir, file);
const 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 (const file of files) {
await new Promise((resolve, reject) => {
const backupPath = path.join(backupDir, file);
const 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';
}
const checksumUrl = App.checksumUrl(app, process.platform, locale);
return this.http.get(checksumUrl)
.map((response) => {
const map = new Map<string, string>();
for (const line of response.text().split('\n')) {
if (line !== '') {
const [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) {
const children = this.findChildren(app);
const hasInstalledChild = children.find((child) => {
return child.isInstalled() && child.parent !== app;
});
if (hasInstalledChild) {
throw new Error('无法卸载,还有依赖此程序的游戏。');
} else if (app.isReady()) {
for (const 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 new Error('应用不是 Ready 状态');
}
if (this.findChildren(app).find((child) => child.isInstalled())) {
console.error('doUninstall', '无法卸载,还有依赖此程序的游戏。', app);
throw new Error('无法卸载,还有依赖此程序的游戏。');
}
app.status.status = 'uninstalling';
const appDir = app.local!.path;
const files = Array.from(app.local!.files.keys()).sort().reverse();
app.status.total = files.length;
// 500毫秒手动刷新,避免文件过多产生的性能问题
const interval = setInterval(() => {
}, 500);
await new Promise((resolve, reject) => {
this.ngZone.runOutsideAngular(async () => {
try {
for (const file of files) {
app.status.progress += 1;
await this.deleteFile(path.join(appDir, file));
}
if (app.parent) {
// TODO: 建立Library模型,把拼路径的事情交给Library
const backupDir = path.join(path.dirname(appDir), 'backup', app.parent.id);
const fileSet = new ComparableSet(files);
const parentSet = new ComparableSet(Array.from(app.parent.local!.files.keys()));
const difference = parentSet.intersection(fileSet);
if (difference) {
await this.restoreFiles(appDir, backupDir, Array.from(difference));
}
}
resolve();
} 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;
const y = screen.availHeight - height;
const 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!;
});
const urlt = new URL(url, window.location.toString());
urlt.searchParams.set('data', data_str);
littleWindow.loadURL(urlt.toString());
remote.ipcMain.on('massage', () => {
alert('from littleWindow');
});
}
}
@import "~font-awesome/css/font-awesome.min.css";
@import "~bootstrap/dist/css/bootstrap.css";
@import "~candy/res/default.css";
@import "~candy-shop/notifyme/candy.css";
@import "~candy-shop/namecomplete/candy.css";
@import "~candy-shop/modify-role/candy.css";
/* Turn on custom 8px wide scrollbar */ /* Turn on custom 8px wide scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; /* 1px wider than Lion. */ width: 8px; /* 1px wider than Lion. */
......
<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 {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
NgZone,
OnChanges,
OnInit,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import * as crypto from 'crypto';
import * as $ from 'jquery';
import {App} from '../app';
import {LoginService} from '../login.service';
import {Candy, CandyFix, CandyShop} from './candy';
declare const Zone;
@Component({
selector: 'mycard-candy',
templateUrl: './candy.component.html',
styleUrls: ['./candy.component.css'],
encapsulation: ViewEncapsulation.Native,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CandyComponent implements OnInit, OnChanges {
@Input()
currentApp: App;
jid: string;
password: string;
nickname: string;
// ismin_window:Boolean=false;
// ismax_window:Boolean=false;
height_default_window = '230px';
root = this.element.nativeElement.shadowRoot;
constructor(private element: ElementRef, private _ngZone: NgZone, private loginService: LoginService) {
this.jid = this.loginService.user.username + '@mycard.moe';
this.password = this.loginService.user.external_id.toString();
this.nickname = this.loginService.user.username;
}
ngOnInit() {
console.log(this.root);
this._ngZone.runOutsideAngular(() => {
Zone.current.fork({
name: 'candy'
}).run(() => {
CandyFix(this.element.nativeElement.shadowRoot, this.jid, this.password, this.nickname);
Candy.init('wss://chat.mycard.moe:5280/websocket', {
core: {
autojoin: this.currentApp.conference && [this.currentApp.conference + '@conference.mycard.moe'],
resource: 'mycard-' + crypto.randomBytes(4).toString('hex')
},
view: {
assets: 'res/',
language: 'cn',
enableXHTML: true
}
});
CandyShop.NotifyMe.init();
CandyShop.NameComplete.init();
CandyShop.ModifyRole.init();
CandyShop.MeDoes.init();
CandyShop.Notifications.init();
CandyShop.Refocus.init();
Candy.Core.connect(this.jid, this.password, this.nickname);
});
});
}
ngOnChanges(changes: SimpleChanges): void {
if (!Candy.Core.getConnection()) {
return;
}
let conference = changes['currentApp'].currentValue.conference;
if (!conference) {
return;
}
conference += '@conference.mycard.moe';
try {
if (Candy.View.Pane.Chat.rooms[conference]) {
Candy.View.Pane.Room.show(conference);
} else {
Candy.Core.Action.Jabber.Room.Join(conference);
}
} catch (error) {
}
}
minimize(): void {
// let minimize:HTMLElement = $('#minimize')[0];
// let maximized:HTMLElement = $('#maximized')[0];
// let un_minimize:HTMLElement = $('#un_minimize')[0];
// let un_maximized:HTMLElement = $('#un_maximized')[0];
console.log(1)
$('#candy', this.root).attr('data-minormax', 'min');
document.getElementById('candy-wrapper')!.style.height = '31px';
$('#mobile-roster-icon', this.root).css('display', 'none');
$('#chat-toolbar', this.root).css('display', 'none');
$('#chat-rooms', this.root).css('display', 'none');
$('#context-menu', this.root).css('display', 'none');
$('#mobile-roster-icon', this.root).css('display', 'none');
$('#minimize', this.root).hide();
$('#unminimize', this.root).show();
$('#restore', this.root).hide();
$('#maximize', this.root).show();
}
restore(): void {
$('#candy', this.root).attr('data-minormax', 'default');
document.getElementById('candy-wrapper')!.style!.height = this.height_default_window;
$('#mobile-roster-icon', this.root).css('display', 'block');
$('#chat-toolbar', this.root).css('display', 'block');
$('#chat-rooms', this.root).css('display', 'block');
$('#context-menu', this.root).css('display', 'block');
$('#mobile-roster-icon', this.root).css('display', 'block');
$('#minimize', this.root).show();
$('#unminimize', this.root).hide();
$('#restore', this.root).hide();
$('#maximize', this.root).show();
}
maximize(): void {
$('#candy', this.root).attr('data-minormax', 'max');
document.getElementById('candy-wrapper')!.style!.height = 'calc( 100% - 180px )';
$('#mobile-roster-icon', this.root).css('display', 'block');
$('#chat-toolbar', this.root).css('display', 'block');
$('#chat-rooms', this.root).css('display', 'block');
$('#context-menu', this.root).css('display', 'block');
$('#mobile-roster-icon', this.root).css('display', 'block');
$('#minimize', this.root).show();
$('#unminimize', this.root).hide();
$('#restore', this.root).show();
$('#maximize', this.root).hide();
}
}
import * as Candy from 'candy';
import * as MeDoes from 'candy-shop/me-does/candy.js';
import * as ModifyRole from 'candy-shop/modify-role/candy.js';
import * as NameComplete from 'candy-shop/namecomplete/candy.js';
import * as Notifications from 'candy-shop/notifications/candy.js';
import * as NotifyMe from 'candy-shop/notifyme/candy.js';
import * as Refocus from 'candy-shop/refocus/candy.js';
import {Base64, Mustache} from 'candy/libs.bundle.js';
import * as jQuery from 'jquery';
declare const Zone;
// import 'zone.js';
declare const requestIdleCallback: Function;
const $: any = jQuery;
let shadow: ShadowRoot;
export {Candy};
export const CandyShop = {MeDoes, ModifyRole, NameComplete, Notifications, NotifyMe, Refocus};
export function CandyFix(element: ShadowRoot, jid: string, password: string, nickname: string) {
shadow = element;
Candy.View.Template.Login.form = `
<form method="post" id="login-form" class="login-form">
<input type="hidden" id="nickname" name="nickname" value="${nickname}"/>
{{#displayUsername}}
<input type="hidden" id="username" name="username" value="${jid}"/>
{{#displayDomain}}
<span class="at-symbol">@</span>
<select id="domain" name="domain">{{#domains}}<option value="{{domain}}">{{domain}}</option>{{/domains}}</select>
{{/displayDomain}}
{{/displayUsername}}
{{#presetJid}}<input type="hidden" id="username" name="username" value="{{presetJid}}"/>{{/presetJid}}
{{#displayPassword}}<input type="hidden" id="password" name="password" value="${password}"/>{{/displayPassword}}
<input type="submit" class="button" value="{{_loginSubmit}}" />
</form>`;
}
Base64.encode = (data: string) => Buffer.from(data).toString('base64');
Base64.decode = (data: string) => Buffer.from(data, 'base64').toString();
$.fn.init = new Proxy($.fn.init, {
construct(target, argumentsList, newTarget) {
// tslint:disable-next-line
let [selector, context, root] = argumentsList;
if (Zone.current.name === 'candy') {
if (selector === 'body') {
selector = shadow;
} else if (selector === document) {
selector = shadow.getElementById('candy');
} else if (!context) {
context = shadow;
}
}
return new target(selector, context, root);
}
});
$.contains = new Proxy(jQuery.contains, {
apply(target, thisArg, argumentsList) {
// tslint:disable-next-line
let [context, elem] = argumentsList;
if (Zone.current.name === 'candy') {
if (context === document) {
context = shadow;
}
}
return target.call(thisArg, context, elem);
}
});
Candy.Util.getPosLeftAccordingToWindowBounds = new Proxy(Candy.Util.getPosLeftAccordingToWindowBounds, {
apply(target, thisArg, argumentsList) {
argumentsList[1] -= this.element.nativeElement.shadowRoot.host.getBoundingClientRect().left;
return target.apply(thisArg, argumentsList);
}
});
Candy.Util.getPosTopAccordingToWindowBounds = new Proxy(Candy.Util.getPosTopAccordingToWindowBounds, {
apply(target, thisArg, argumentsList) {
argumentsList[1] -= this.element.nativeElement.shadowRoot.host.getBoundingClientRect().top;
return target.apply(thisArg, argumentsList);
}
});
// 性能优化:禁用加入动画
Candy.View.Pane.Roster.joinAnimation = function () {
};
// 性能优化:禁用用户排序
Candy.View.Pane.Roster._insertUser = function (roomJid: string, roomId: string, user: any, userId: string, currentUser: any) {
const contact = user.getContact();
const html = Mustache.to_html(Candy.View.Template.Roster.user, {
roomId: roomId,
userId: userId,
userJid: user.getJid(),
realJid: user.getRealJid(),
status: user.getStatus(),
contact_status: contact ? contact.getStatus() : 'unavailable',
nick: user.getNick(),
displayNick: Candy.Util.crop(user.getNick(), Candy.View.getOptions().crop.roster.nickname),
role: user.getRole(),
affiliation: user.getAffiliation(),
me: currentUser !== undefined && user.getNick() === currentUser.getNick(),
tooltipRole: $.i18n._('tooltipRole'),
tooltipIgnored: $.i18n._('tooltipIgnored')
});
const rosterPane = Candy.View.Pane.Room.getPane(roomJid, '.roster-pane');
rosterPane.append(html);
};
// 性能优化:将未读消息计数的的 jQuery show() 改为直接置 style
Candy.View.Pane.Chat.increaseUnreadMessages = function (roomJid: string) {
const unreadElem = this.getTab(roomJid).find('.unread');
unreadElem.text(unreadElem.text() !== '' ? parseInt(unreadElem.text(), 10) + 1 : 1);
unreadElem[0].style.display = 'inherit';
// only increase window unread messages in private chats
if (Candy.View.Pane.Chat.rooms[roomJid].type === 'chat' || Candy.View.getOptions().updateWindowOnAllMessages === true) {
Candy.View.Pane.Window.increaseUnreadMessages();
}
};
// 性能优化:将收到消息时的滚动放进requestIdleCallback
Candy.View.Pane.Message.show = function (roomJid: any, name: any, message: any, xhtmlMessage: any, timestamp: any,
from: any, carbon: any, stanza: any) {
message = Candy.Util.Parser.all(message.substring(0, Candy.View.getOptions().crop.message.body));
if (Candy.View.getOptions().enableXHTML === true && xhtmlMessage) {
xhtmlMessage = Candy.Util.parseAndCropXhtml(xhtmlMessage, Candy.View.getOptions().crop.message.body);
}
timestamp = timestamp || new Date();
// Assume we have an ISO-8601 date string and convert it to a Date object
if (!timestamp.toDateString) {
timestamp = Candy.Util.iso8601toDate(timestamp);
}
// Before we add the new message, check to see if we should be automatically scrolling or not.
const messagePane = Candy.View.Pane.Room.getPane(roomJid, '.message-pane');
let enableScroll;
if (stanza && stanza.children('delay').length > 0) {
enableScroll = true;
} else {
enableScroll =
messagePane.scrollTop() + messagePane.outerHeight() === messagePane.prop('scrollHeight') || !$(messagePane).is(':visible');
}
Candy.View.Pane.Chat.rooms[roomJid].enableScroll = enableScroll;
const evtData: any = {
roomJid: roomJid,
name: name,
message: message,
xhtmlMessage: xhtmlMessage,
from: from,
stanza: stanza
};
if ($(Candy).triggerHandler('candy:view.message.before-show', evtData) === false) {
return;
}
message = evtData.message;
xhtmlMessage = evtData.xhtmlMessage;
if (xhtmlMessage !== undefined && xhtmlMessage.length > 0) {
message = xhtmlMessage;
}
if (!message) {
return;
}
const renderEvtData = {
template: Candy.View.Template.Message.item,
templateData: {
name: name,
displayName: Candy.Util.crop(name, Candy.View.getOptions().crop.message.nickname),
message: message,
time: Candy.Util.localizedTime(timestamp),
timestamp: timestamp.toISOString(),
roomjid: roomJid,
from: from
},
stanza: stanza
};
$(Candy).triggerHandler('candy:view.message.before-render', renderEvtData);
const html = Mustache.to_html(renderEvtData.template, renderEvtData.templateData);
Candy.View.Pane.Room.appendToMessagePane(roomJid, html);
const elem = Candy.View.Pane.Room.getPane(roomJid, '.message-pane').children().last();
// click on username opens private chat
elem.find('a.label').click(function (event: any) {
event.preventDefault();
// Check if user is online and not myCandy.View.Pane
const room = Candy.Core.getRoom(roomJid);
if (room &&
name !== Candy.View.Pane.Room.getUser(Candy.View.getCurrent().roomJid).getNick() &&
room.getRoster().get(roomJid + '/' + name)) {
if (Candy.View.Pane.PrivateRoom.open(roomJid + '/' + name, name, true) === false) {
return false;
}
}
});
if (!carbon) {
const notifyEvtData = {
name: name,
displayName: Candy.Util.crop(name, Candy.View.getOptions().crop.message.nickname),
roomJid: roomJid,
message: message,
time: Candy.Util.localizedTime(timestamp),
timestamp: timestamp.toISOString()
};
$(Candy).triggerHandler('candy:view.message.notify', notifyEvtData);
// Check to see if in-core notifications are disabled
if (!Candy.Core.getOptions().disableCoreNotifications) {
if (Candy.View.getCurrent().roomJid !== roomJid || !Candy.View.Pane.Window.hasFocus()) {
Candy.View.Pane.Chat.increaseUnreadMessages(roomJid);
if (!Candy.View.Pane.Window.hasFocus()) {
// Notify the user about a new private message OR on all messages if configured
if (Candy.View.Pane.Chat.rooms[roomJid].type === 'chat' || Candy.View.getOptions().updateWindowOnAllMessages === true) {
Candy.View.Pane.Chat.Toolbar.playSound();
}
}
}
}
if (Candy.View.getCurrent().roomJid === roomJid) {
requestIdleCallback(function () {
Candy.View.Pane.Room.scrollToBottom(roomJid);
});
}
}
evtData.element = elem;
$(Candy).triggerHandler('candy:view.message.after-show', evtData);
};
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
} }
webview { webview {
flex-grow: 1; flex: 1
} }
<webview src="https://ygobbs.com" (new-window)="shell.openExternal($event.url)"></webview>
import {Component, OnInit} from '@angular/core';
import {shell} from 'electron';
@Component({
selector: 'mycard-community',
templateUrl: './community.component.html',
styleUrls: ['./community.component.css']
})
export class CommunityComponent implements OnInit {
shell = shell;
constructor() {
}
ngOnInit() {
}
}
: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: 14px;
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: 14px;
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;*/
/*}*/
<!-- Begin page content -->
<div #nav id="nav-wrapper" class="resize-wrapper resize-right">
<nav id="apps" *ngIf="apps" class="bg-faded sidebar scroll">
<div id="search" class="input-group">
<i class="fa fa-search input-group-addon search" id="basic-addon1"></i>
<input #search id="search-input" type="text" class="form-control search" placeholder="搜索游戏" aria-describedby="basic-addon1">
</div>
<span i18n *ngIf="grouped_apps.installed">已安装</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="nav-link" [class.active]="app===currentApp" [href]="'https://mycard.moe/' + app.id">
<img *ngIf="app.icon" class="icon" [src]="app.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="pie" [class.second-half]="app.status.progress/app.status.total>0.5">
<div class="left-side half-circle" [style.transform]="'rotate('+(app.status.progress/app.status.total).toString()+'turn)'"></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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.recommend">推荐</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.mysterious">迷之物体</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.touhou">东方 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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.touhou_pc98">东方旧作</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
<span i18n *ngIf="grouped_apps.runtime_installed">已安装的运行库</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)" [href]="'https://mycard.moe/' + app.id" class="nav-link" [class.active]="app===currentApp">
<img *ngIf="app.icon" class="icon" [src]="app.icon">
{{app.name}}
</a>
</li>
</ul>
</nav>
<div class="resize" (mousedown)="mousedown($event)"></div>
</div>
<div id="right">
<div id="main">
<mycard-app-detail class="scroll" *ngIf="currentApp" [currentApp]="currentApp"></mycard-app-detail>
<!--<roster class="scroll"></roster>-->
</div>
<div id="candy-wrapper" class="resize-wrapper resize-top" style="max-height: calc( 100% - 180px )">
<div class="resize" (mousedown)="mousedown($event)"></div>
<mycard-candy *ngIf="currentApp" [currentApp]="currentApp"></mycard-candy>
</div>
</div>
<div id="right-shadow"></div>
import {ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {shell} from 'electron';
import * as $ from 'jquery';
import {App, Category} from '../app';
import {AppsService} from '../apps.service';
import {LoginService} from '../login.service';
import {SettingsService} from '../settings.service';
const ReconnectingWebSocket = require('reconnecting-websocket');
declare const Zone;
@Component({
selector: 'mycard-lobby',
templateUrl: './lobby.component.html',
styleUrls: ['./lobby.component.css']
})
export class LobbyComponent implements OnInit {
currentApp: App;
apps: Map<string, App>;
resizing: HTMLElement | undefined;
offset: number;
@ViewChild('search')
search: ElementRef;
private messages: WebSocket;
constructor(private appsService: AppsService, private loginService: LoginService,
private settingsService: SettingsService, private ref: ChangeDetectorRef) {
}
get grouped_apps(): any {
const contains = ['game', 'music', 'book'].map((value) => Category[value]);
const result = {runtime: []};
for (const app of this.apps.values()) {
let tag: string;
if (contains.includes(app.category)) {
if (app.isInstalled()) {
tag = 'installed';
} else {
tag = app.tags ? app.tags[0] : 'test';
}
} else {
if (app.isInstalled()) {
tag = 'runtime_installed';
} else {
tag = 'runtime';
}
}
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 (const 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();
const url = new URL('wss://api.moecube.com:3100');
const params: URLSearchParams = url.searchParams;
params.set('user_id', this.loginService.user.email);
this.messages = new ReconnectingWebSocket(url);
this.messages.onmessage = async (event) => {
const 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;
const main_height = event.clientY - document.getElementById('navbar')!.clientHeight;
// console.log(event.clientY);
Zone.current.fork({
name: 'candy'
}).run(() => {
if (height > 150 && main_height > 180) {
if (height < 230) {
height = 230;
}
this.resizing.style.height = `${height}px`;
if ($('#candy', document).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;
}
}
chooseApp(app: App) {
this.currentApp = app;
this.appsService.lastVisited = app;
}
openExternal(url: string) {
shell.openExternal(url);
}
}
/**
* Created by zh99998 on 2016/10/25.
*/
import {Injectable} from '@angular/core';
import * as crypto from 'crypto';
import {fromPairs} from 'lodash';
export interface User {
admin: boolean;
avatar_url: string;
email: string;
external_id: number;
moderator: boolean;
name: string;
username: string;
}
@Injectable()
export class LoginService {
user: User;
logged_in = false;
logging_out = false;
readonly return_sso_url = 'https://mycard.moe/login_callback'; // 这个url不会真的被使用,可以填写不存在的
constructor() {
const data = localStorage.getItem('login');
if (data) {
this.user = JSON.parse(data);
this.logged_in = true;
}
}
logout() {
this.logging_out = true;
this.logged_in = false;
localStorage.removeItem('login');
}
loginUrl() {
const params = new URLSearchParams();
params.set('return_sso_url', this.return_sso_url);
const payload = Buffer.from(params.toString()).toString('base64');
const url = new URL('https://accounts.moecube.com');
url.searchParams.set('sso', payload);
url.searchParams.set('sig', crypto.createHmac('sha256', 'zsZv6LXHDwwtUAGa').update(payload).digest('hex'));
return url.toString();
}
logoutUrl() {
const url = new URL('https://accounts.moecube.com/logout');
url.searchParams.set('redirect', 'https://mycard.moe/logout_callback');
return url.toString();
}
return_sso(return_url: string) {
if (return_url === 'https://mycard.moe/logout_callback') {
return location.reload();
}
if (!return_url.startsWith(this.return_sso_url)) {
return;
}
const token = new URL(return_url)['searchParams'].get('sso');
if (!token) {
return;
}
this.handleLogin(token);
}
handleLogin(token: string) {
this.user = <any>fromPairs(Array.from(new URLSearchParams(Buffer.from(token, 'base64').toString())));
this.logged_in = true;
localStorage.setItem('login', JSON.stringify(this.user));
}
}
<webview [src]="loginService.logging_out ? loginService.logoutUrl() : loginService.loginUrl()"
(will-navigate)="loginService.return_sso($event.url)"
(did-get-redirect-request)="loginService.return_sso($event.newURL)"
(new-window)="shell.openExternal($event.url)"></webview>
import {Component, OnInit} from '@angular/core';
import {shell} from 'electron';
import {LoginService} from '../login.service';
@Component({
selector: 'mycard-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
shell = shell;
constructor(public loginService: LoginService) {
}
ngOnInit() {
}
}
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
children: []
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class MyCardRoutingModule {
}
import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CandyComponent } from './candy/candy.component';
import { MyCardRoutingModule } from './mycard-routing.module';
import { MyCardComponent } from './mycard/mycard.component';
import { NavbarComponent } from './navbar/navbar.component';
import { SplitPaneModule } from 'ng2-split-pane/lib/ng2-split-pane';
import { LobbyComponent } from './lobby/lobby.component';
import { AppDetailComponent } from './app-detail/app-detail.component';
import { NetworkComponent } from './network/network.component';
import { RosterComponent } from './roster/roster.component';
import { YGOProComponent } from './ygopro/ygopro.component';
import 'rxjs/Rx';
import {AppsService} from './apps.service';
import {DownloadService} from './download.service';
import {LoginService} from './login.service';
import {SettingsService} from './settings.service';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';
import { LoginComponent } from './login/login.component';
import { CommunityComponent } from './community/community.component';
@NgModule({
declarations: [
MyCardComponent,
CandyComponent,
NavbarComponent,
LobbyComponent,
AppDetailComponent,
NetworkComponent,
RosterComponent,
YGOProComponent,
LoginComponent,
CommunityComponent
],
imports: [
BrowserModule,
MyCardRoutingModule,
SplitPaneModule,
FormsModule,
HttpModule
],
providers: [AppsService, DownloadService, LoginService, SettingsService],
bootstrap: [MyCardComponent],
schemas: [NO_ERRORS_SCHEMA]
})
export class MyCardModule {
}
:host {
height: 100%;
display: flex;
flex-direction: column;
}
/*:host {*/
/*background-color: white;*/
/*}*/
.page {
/*display: block;*/
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;
}
#navbar {
background-color: #f7f7f9!important;
padding: 0;
}
#navbar-brand {
color: #00a4d9;
font-size: 24px;
width: 190px;
margin: 0;
text-align: center;
}
#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;
}
<nav id="navbar" class="navbar navbar-toggleable-md navbar-light">
<a id="navbar-brand" class="navbar-brand" href="#">MyCard</a>
<ul class="navbar-nav mr-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">
<div id="update-status">
<i #error [hidden]="update_status != 'error'" (click)="update_retry()" class="fa fa-exclamation-circle"
data-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-toggle="tooltip" i18n-title title="正在检查更新"></i>
<i #update_available [hidden]="update_status != 'update-available'" class="fa fa-refresh fa-spin"
data-toggle="tooltip" i18n-title title="正在下载更新"></i>
<i #update_downloaded [hidden]="update_status != 'update-downloaded'" (click)="update_install()"
class="fa fa-angle-double-up" data-toggle="tooltip" i18n-title title="下载更新完成,点击安装"></i>
</div>
<div 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-toggle="modal" data-target="#settings-modal" class="fa fa-cog item-icon" aria-hidden="true"
i18n-title title="设置"></i>
</div>
<div id="border" *ngIf="platform === 'win32'">|</div>
<div id="window-buttons" *ngIf="platform === 'win32'">
<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>
</div>
</div>
</nav>
<mycard-lobby class="page" *ngIf="loginService.logged_in" [hidden]="currentPage != 'lobby'"></mycard-lobby>
<webview class="page" [src]="loginService.logging_out ? loginService.logoutUrl() : loginService.loginUrl()"
[hidden]="loginService.logged_in || loginService.logging_out"
(will-navigate)="loginService.return_sso($event.url)"
(did-get-redirect-request)="loginService.return_sso($event.newURL)"
(new-window)="openExternal($event.url)"></webview>
<webview class="page" [hidden]="currentPage != 'community'" src="https://ygobbs.com"
(new-window)="openExternal($event.url)"></webview>
<!-- 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="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</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-dismiss="modal">取消</button>
<button i18n type="submit" class="btn btn-primary">确定</button>
</div>
</form>
</div>
</div>
</div>
import { ChangeDetectorRef, Component, ElementRef, OnInit, Renderer, ViewChild } from '@angular/core';
import 'bootstrap';
import { remote, shell } from 'electron';
import * as $ from 'jquery';
import * as Tether from 'tether';
import { LoginService } from '../login.service';
import { SettingsService } from '../settings.service';
window['Tether'] = Tether;
const autoUpdater: Electron.AutoUpdater = remote.getGlobal('autoUpdater');
@Component({
selector: 'mycard-root',
templateUrl: './mycard.component.html',
styleUrls: ['./mycard.component.css']
})
export class MyCardComponent implements OnInit {
currentPage = 'lobby';
update_status: string | undefined = remote.getGlobal('update_status');
update_error: string | undefined;
currentWindow = remote.getCurrentWindow();
window = window;
@ViewChild('error')
error: ElementRef;
@ViewChild('checking_for_update')
checking_for_update: ElementRef;
@ViewChild('update_available')
update_available: ElementRef;
@ViewChild('update_downloaded')
update_downloaded: ElementRef;
update_elements: Map<string, ElementRef>;
locale: string;
resizing: HTMLElement | null;
@ViewChild('moesound')
moesound: ElementRef;
platform = process.platform
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
}));
// document.addEventListener('drop', (event)=>{
// console.log('drop', event);
// event.preventDefault();
//
// });
}
constructor(private renderer: Renderer, 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());
autoUpdater.on('error', (error) => {
this.set_update_status('error');
});
autoUpdater.on('checking-for-update', () => {
this.set_update_status('checking-for-update');
});
autoUpdater.on('update-available', () => {
this.set_update_status('update-available');
});
autoUpdater.on('update-not-available', () => {
this.set_update_status('update-not-available');
});
autoUpdater.on('update-downloaded', (event) => {
this.set_update_status('update-downloaded');
});
this.locale = this.settingsService.getLocale();
}
update_retry() {
autoUpdater.checkForUpdates();
}
update_install() {
autoUpdater.quitAndInstall();
}
set_update_status(status: string) {
console.log('autoUpdater', status);
if (this.update_status) {
const element = this.update_elements.get(this.update_status);
if (element) {
$(element.nativeElement).tooltip('dispose');
}
}
this.update_status = status;
this.ref.detectChanges();
const element = this.update_elements.get(this.update_status);
if (element) {
$(element.nativeElement).tooltip({ placement: 'bottom', container: 'body' });
}
}
openExternal(url: string) {
shell.openExternal(url);
}
submit() {
if (this.locale !== this.settingsService.getLocale()) {
localStorage.setItem(SettingsService.SETTING_LOCALE, this.locale);
remote.app.relaunch();
remote.app.quit();
}
}
//
// moesound_loaded() {
// this.moesound.nativeElement.insertCSS(`
// body > section > header, #bjax-target > div.row.m-t-lg.m-b-lg, #bjax-target > section {
// display: none;
// }
// body > section > section {
// top: 0!important;
// }
// `);
// }
//
// moesound_newwindow(url: string) {
// console.log(url);
// }
}
nav {
-webkit-app-region: drag
}
<nav class="navbar navbar-toggleable-md mb-4">
<a class="navbar-brand" href="#">MyCard</a>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
<form class="form-inline mt-2 mt-md-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'mycard-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.css']
})
export class NavbarComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
<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)" (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)="clipboard.writeText(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-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>
import {Component, Input} from '@angular/core';
import {clipboard} from 'electron';
import {App} from '../app';
import {AppsService} from '../apps.service';
@Component({
selector: 'mycard-network',
templateUrl: './network.component.html',
styleUrls: ['./network.component.css']
})
export class NetworkComponent {
clipboard = clipboard;
@Input()
currentApp: App;
constructor(public appsService: AppsService) {
console.log('constructor');
}
}
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'mycard-roster',
templateUrl: './roster.component.html',
styleUrls: ['./roster.component.css']
})
export class RosterComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
/**
* Created by weijian on 2016/10/24.
*/
import {Injectable} from '@angular/core';
import {remote} from 'electron';
import * as path from 'path';
export interface Library {
'default': boolean;
path: string;
}
@Injectable()
export class SettingsService {
static SETTING_LIBRARY = 'library';
static defaultLibraries = [
{
'default': true,
path: path.join(remote.app.getPath('appData'), 'MyCardLibrary')
},
];
static SETTING_LOCALE = 'locale';
static defaultLocale = remote.app.getLocale();
locale: string;
libraries: Library[];
getLibraries () {
if (!this.libraries) {
const data = localStorage.getItem(SettingsService.SETTING_LIBRARY);
if (!data) {
this.libraries = SettingsService.defaultLibraries;
localStorage.setItem(SettingsService.SETTING_LIBRARY,
JSON.stringify(SettingsService.defaultLibraries));
} else {
this.libraries = JSON.parse(data);
}
}
return this.libraries;
}
addLibrary (libraryPath: string, isDefault: boolean) {
const libraries = this.getLibraries();
if (isDefault) {
libraries.forEach((l) => {
l.default = false;
});
}
libraries.push({'default': isDefault, path: libraryPath});
this.libraries = libraries;
localStorage.setItem(SettingsService.SETTING_LIBRARY, JSON.stringify(libraries));
}
setDefaultLibrary (library: Library) {
const libraries = this.getLibraries();
libraries.forEach((l) => {
l.default = library.path === l.path;
});
this.libraries = libraries;
localStorage.setItem(SettingsService.SETTING_LIBRARY, JSON.stringify(libraries));
}
getDefaultLibrary (): Library {
if (!this.libraries) {
this.getLibraries();
}
const result = this.libraries.find((item) => item.default === true);
if (result) {
return result;
} else {
throw new Error(('no default library found'));
}
}
getLocale (): string {
if (!this.locale) {
const locale = localStorage.getItem(SettingsService.SETTING_LOCALE);
if (!locale) {
this.locale = SettingsService.defaultLocale;
localStorage.setItem(SettingsService.SETTING_LOCALE, SettingsService.defaultLocale);
} else {
this.locale = locale;
}
}
return this.locale;
}
setLocale (locale: string) {
this.locale = locale;
localStorage.setItem(SettingsService.SETTING_LOCALE, locale);
}
}
...@@ -11,7 +11,7 @@ export class ComparableSet<T> extends Set<T> { ...@@ -11,7 +11,7 @@ export class ComparableSet<T> extends Set<T> {
} }
isSuperset(subset: Set<T>) { isSuperset(subset: Set<T>) {
for (let elem of subset) { for (const elem of subset) {
if (!this.has(elem)) { if (!this.has(elem)) {
return false; return false;
} }
...@@ -20,16 +20,16 @@ export class ComparableSet<T> extends Set<T> { ...@@ -20,16 +20,16 @@ export class ComparableSet<T> extends Set<T> {
} }
union(setB: Set<T>): Set<T> { union(setB: Set<T>): Set<T> {
let union = new Set(this); const union = new Set(this);
for (let elem of setB) { for (const elem of setB) {
union.add(elem); union.add(elem);
} }
return union; return union;
} }
intersection(setB: Set<T>): Set<T> { intersection(setB: Set<T>): Set<T> {
let intersection = new Set(); const intersection = new Set();
for (let elem of setB) { for (const elem of setB) {
if (this.has(elem)) { if (this.has(elem)) {
intersection.add(elem); intersection.add(elem);
} }
...@@ -38,8 +38,8 @@ export class ComparableSet<T> extends Set<T> { ...@@ -38,8 +38,8 @@ export class ComparableSet<T> extends Set<T> {
} }
difference(setB: Set<T>): Set<T> { difference(setB: Set<T>): Set<T> {
let difference = new Set(this); const difference = new Set(this);
for (let elem of setB) { for (const elem of setB) {
difference.delete(elem); difference.delete(elem);
} }
return difference; return difference;
......
<div *ngIf="!matching" id="action">
<button [disabled]="!appsService.allReady(app)" (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)" (click)="request_match('entertain')" type="button" class="btn btn-secondary btn-sm">娱乐匹配</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-target="#game-list-modal">自定义游戏</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-target="#game-create-windbot">单人模式</button>
<button i18n [disabled]="!appsService.allReady(app)" type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-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="input-group input-group-sm">
<label i18n class="input-group-addon" id="basic-addon1">卡组</label>
<select class="form-control form-control-sm" id="exampleSelect1" name="deck" [(ngModel)]="current_deck">
<option *ngFor="let deck of decks" [ngValue]="deck">{{deck}}</option>
</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 class="modal fade" id="game-create-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="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</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-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" 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-toggle="tooltip" data-placement="bottom" [title]="user.username">
</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 *ngIf="room.options.rule != default_options.rule">{{{'0': 'OCG', '1': 'TCG', '2': 'O/T', '3': '专有卡禁止'}[room.options.rule]}}</span>
<span i18n *ngIf="room.options.start_lp != default_options.start_lp">{{room.options.start_lp}} LP</span>
<span i18n *ngIf="room.options.start_hand != default_options.start_hand">{{room.options.start_hand}} 初始</span>
<span i18n *ngIf="room.options.draw_count != default_options.draw_count">{{room.options.draw_count}} 抽卡</span>
<span i18n *ngIf="room.options.enable_priority != default_options.enable_priority">旧规则</span>
<span i18n *ngIf="room.options.no_check_deck != default_options.no_check_deck">不检查</span>
<span i18n *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck">不洗卡</span>
</td>
</tr>
</tbody>
</table>
<div id="join-private" class="input-group input-group-sm">
<i class="fa fa-key input-group-addon" aria-hidden="true"></i>
<input [(ngModel)]="join_password" type="text" class="form-control" placeholder="在这输入你朋友的私密房间密码就可以进去了哦!">
<span class="input-group-btn"><button class="btn btn-secondary" type="button" (click)="join_private(join_password)">加入私密房间</button></span>
</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 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 id="copy-wrapper" class="input-group-btn" data-toggle="tooltip" title="房间密码已复制到剪贴板">
<button class="btn btn-secondary fa fa-clipboard" type="button" title="复制" (click)="copy(host_password, $event)"></button>
</span>
</div>
<small 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">OCG & TCG</option>
<option i18n value="3">专有卡禁止</option>
</select>
</div>
<div class="form-group">
<label i18n for="game-create-rule">决斗模式</label>
<select class="form-control form-control-sm" id="game-create-mode" name="mode" (change)="room.options.start_lp = room.options.mode == 2 ? 16000 : 8000" [(ngModel)]="room.options.mode">
<option i18n value="0">单局模式</option>
<option i18n value="1">比赛模式</option>
<option i18n value="2">TAG</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>
</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="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</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="close" data-dismiss="modal" aria-label="Close">-->
<!--<span aria-hidden="true">&times;</span>-->
<!--</button>-->
<!--</div>-->
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs" role="tablist">
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-toggle="tab" href="#home" role="tab">推荐</a>-->
<!--</li>-->
<li class="nav-item">
<a i18n class="nav-link active" data-toggle="tab" href="#game-replay-watch" role="tab">观战</a>
</li>
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-toggle="tab" href="#home" role="tab">收藏的录像</a>-->
<!--</li>-->
<li class="nav-item">
<a i18n class="nav-link" data-toggle="tab" href="#game-replay-local" role="tab">本地录像</a>
</li>
<!--<li *ngIf="settingsService.getLocale().startsWith('zh')" class="nav-item">-->
<!--<a class="nav-link" data-toggle="tab" href="#game-replay-bilibili" role="tab">哔哩哔哩</a>-->
<!--</li>-->
<!--<li class="nav-item">-->
<!--<a class="nav-link" data-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="title">
<span i18n>游戏模式</span>
<div id="watch-filter" class="dropdown">
<button i18n class="btn btn-secondary dropdown-toggle btn-sm" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
筛选
</button>
<div class="dropdown-menu">
<h6 i18n class="dropdown-header">匹配</h6>
<div class="form-check dropdown-item">
<input id="filter-athletic" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.athletic" (change)="refresh_replay_rooms()">
<label i18n for="filter-athletic" class="form-check-label">竞技匹配</label>
</div>
<div class="form-check dropdown-item">
<input id="filter-entertain" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.entertain" (change)="refresh_replay_rooms()">
<label i18n for="filter-entertain" class="form-check-label">娱乐匹配</label>
</div>
<h6 i18n class="dropdown-header">自定义游戏</h6>
<div class="form-check dropdown-item">
<input id="filter-single" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.single" (change)="refresh_replay_rooms()">
<label i18n for="filter-single" class="form-check-label">单局模式</label>
</div>
<div class="form-check dropdown-item">
<input id="filter-match" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.match" (change)="refresh_replay_rooms()">
<label i18n for="filter-match" class="form-check-label">比赛模式</label>
</div>
<div class="form-check dropdown-item">
<input id="filter-tag" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.tag" (change)="refresh_replay_rooms()">
<label i18n for="filter-tag" class="form-check-label">TAG</label>
</div>
<h6 i18n class="dropdown-header">单人模式</h6>
<div class="form-check dropdown-item">
<input id="filter-windbot" type="checkbox" class="form-check-input" [(ngModel)]="replay_rooms_filter.windbot" (change)="refresh_replay_rooms()">
<label i18n for="filter-windbot" class="form-check-label">单人模式</label>
</div>
</div>
</div>
</th>
<th i18n class="users">游戏标题</th>
<th i18n class="mode">玩家</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 *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-toggle="tooltip" data-placement="bottom" [title]="user.username">
</td>
<td class="extra">
<span *ngIf="!(room.arena || room.id.startsWith('AI#'))">
<span *ngIf="room.options.rule != default_options.rule">{{{'0': 'OCG', '1': 'TCG', '2': 'O/T', '3': '专有卡禁止'}[room.options.rule]}}</span>
<span i18n *ngIf="room.options.start_lp != default_options.start_lp">{{room.options.start_lp}} LP</span>
<span i18n *ngIf="room.options.start_hand != default_options.start_hand">{{room.options.start_hand}} 初始</span>
<span i18n *ngIf="room.options.draw_count != default_options.draw_count">{{room.options.draw_count}} 抽卡</span>
<span i18n *ngIf="room.options.enable_priority != default_options.enable_priority">旧规则</span>
<span i18n *ngIf="room.options.no_check_deck != default_options.no_check_deck">不检查</span>
<span i18n *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck">不洗卡</span>
</span>
</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-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 { Headers, Http } from '@angular/http';
import * as child_process from 'child_process';
import { clipboard, remote, shell } from 'electron';
import * as fs from 'fs-extra';
import * as ini from 'ini';
import { EncodeOptions } from 'ini';
import * as $ from 'jquery';
import * as path from 'path';
import 'rxjs/Rx';
import { ISubscription } from 'rxjs/Subscription';
import { App } from '../app';
import { AppsService } from '../apps.service';
import { LoginService } from '../login.service';
import { SettingsService } from '../settings.service';
import Timer = NodeJS.Timer;
// import WillNavigateEvent = Electron.WillNavigateEvent;
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;
url?: string;
address: string;
port: number;
custom?: boolean;
replay?: boolean;
}
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_hand: number;
draw_count: number;
enable_priority: boolean;
no_check_deck: boolean;
no_shuffle_deck: boolean;
lflist?: number;
time_limit?: number;
}
export interface Points {
exp: number;
exp_rank: number;
pt: number;
arena_rank: number;
win: number;
lose: number;
draw: number;
all: number;
ratio: number;
}
interface YGOProData {
windbot: { [locale: string]: string[] };
}
let matching: ISubscription | undefined;
let matching_arena: string | undefined;
let match_started_at: Date;
@Component({
selector: 'mycard-ygopro',
templateUrl: './ygopro.component.html',
styleUrls: ['./ygopro.component.css']
})
export class YGOProComponent implements OnInit, OnDestroy {
@Input()
app: App;
@Input()
currentApp: App;
@Output()
points: EventEmitter<Points> = new EventEmitter();
decks: string[] = [];
replays: string[] = [];
current_deck: string;
system_conf: string;
numfont: string[];
textfont: string[];
@ViewChild('bilibili')
bilibili: ElementRef;
@ViewChild('youtube')
youtube: ElementRef;
// points: Points;
windbot: string[]; // ["琪露诺", "谜之剑士LV4", "复制植物", "尼亚"];
servers: Server[] = [];
rooms_loading = true;
default_options: Options = {
mode: 1,
rule: this.settingsService.getLocale().startsWith('zh') ? 0 : 1,
start_lp: 8000,
start_hand: 5,
draw_count: 1,
enable_priority: false,
no_check_deck: false,
no_shuffle_deck: false,
lflist: 0,
time_limit: 180
};
room: Room = { title: this.loginService.user.username + '的房间', options: Object.assign({}, this.default_options) };
rooms: 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: ISubscription | 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: Http, public appsService: AppsService, private loginService: LoginService,
public settingsService: SettingsService, private ref: ChangeDetectorRef) {
switch (process.platform) {
case 'darwin':
this.numfont = ['/System/Library/Fonts/SFNSTextCondensed-Bold.otf'];
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);
}
if (this.settingsService.getLocale().startsWith('zh')) {
this.servers.push({
id: 'tiramisu',
url: 'wss://tiramisu.mycard.moe:7923',
address: '112.124.105.11',
port: 7911,
custom: true,
replay: true
}, {
id: 'tiramisu-athletic',
url: 'wss://tiramisu.mycard.moe:8923',
address: '112.124.105.11',
port: 8911,
custom: false,
replay: true
});
} else {
this.servers.push({
id: 'mercury-us-1-athletic',
url: 'wss://mercury-us-1.mycard.moe:7923',
address: '104.237.154.173',
port: 7911,
custom: true,
replay: true
}, {
id: 'mercury-us-1',
url: 'wss://mercury-us-1.mycard.moe:7923',
address: '104.237.154.173',
port: 8911,
custom: false,
replay: true
});
}
}
refresh_replay_rooms() {
this.replay_rooms_show = this.replay_rooms.filter((room) => {
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;
});
}
async ngOnInit() {
let locale: string;
if (this.settingsService.getLocale().startsWith('zh')) {
locale = 'zh-CN';
} else {
locale = 'en-US';
}
this.windbot = (<YGOProData>this.app.data).windbot[locale];
this.system_conf = path.join(this.app.local!.path, 'system.conf');
await this.refresh();
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);
};
connection.onerror = (event: ErrorEvent) => {
console.log('error', server.id, event);
this.rooms = this.rooms.filter(room => room.server !== server);
};
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.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 = [];
});
remote.ipcMain.on('YGOPro', (e: any, type: string) => {
console.log('rrrrr');
this.request_match(type);
});
}
async refresh() {
this.decks = await this.get_decks();
let system_conf = await this.load_system_conf();
if (this.decks.includes(system_conf.lastdeck)) {
this.current_deck = system_conf.lastdeck;
} else {
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('https://mycard.moe/ygopro/api/user', {
search: {
username: this.loginService.user.username
}
})
.map((response) => response.json())
.toPromise();
this.points.emit(points);
} catch (error) {
console.log(error);
}
};
async get_decks(): Promise<string[]> {
try {
let files: string[] = await fs.readdir(path.join(this.app.local!.path, 'deck'));
return files.filter(file => path.extname(file) === '.ydk').map(file => path.basename(file, '.ydk'));
} catch (error) {
return [];
}
}
async get_replays(): Promise<string[]> {
try {
let files: string[] = await fs.readdir(path.join(this.app.local!.path, 'replay'));
return files.filter(file => path.extname(file) === '.yrp').map(file => path.basename(file, '.yrp'));
} catch (error) {
return [];
}
}
async get_font(files: string[]): Promise<string | undefined> {
for (let file of files) {
if (await fs.pathExists(file)) {
return file;
}
}
}
async delete_deck(deck: string) {
if (confirm('确认删除?')) {
try {
await fs.unlink(path.join(this.app.local!.path, 'deck', 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<SystemConf> {
let data = await fs.readFile(this.system_conf, { encoding: 'utf-8' });
return ini.parse(data);
};
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(['-j']);
};
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]);
}
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')]);
}
join_windbot(name?: string) {
if (!name) {
name = this.windbot[Math.floor(Math.random() * this.windbot.length)];
}
return this.join('AI#' + name, this.servers[0]);
}
async start_game(args: string[]) {
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((resolve, reject) => {
let child = child_process.spawn(path.join(this.app.local!.path, this.app.actions.get('main')!.execute), args, {
cwd: this.app.local!.path,
stdio: 'inherit'
});
child.on('error', (error) => {
reject(error);
win.restore();
});
child.on('exit', async (code, signal) => {
// error 触发之后还可能会触发exit,但是Promise只承认首次状态转移,因此这里无需重复判断是否已经error过。
await this.refresh();
resolve();
win.restore();
});
try {
this.http.get('https://mycard.moe/ygopro/api/history', {
search: {
page: 1,
username: this.loginService.user.username,
type: 0,
page_num: 1
}
})
.map((response) => response.json())
.toPromise()
.then((d) => {
start_time = d.data[0].start_time;
});
} catch (error) {
console.log(error);
}
try {
this.http.get('https://api.mycard.moe/ygopro/arena/user', { search: { username: this.loginService.user.username } })
.map((response) => response.json())
.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('https://mycard.moe/ygopro/api/history', {
search: {
page: 1,
username: this.loginService.user.username,
// username: "星光pokeboy",
type: 0,
page_num: 1
}
})
.map((response) => response.json())
.toPromise()
.then((d) => {
data = d.data[0];
data.myname = this.loginService.user.username;
});
await this.http.get('https://api.mycard.moe/ygopro/arena/user', {
search: {
username: this.loginService.user.username,
}
})
.map((response) => response.json())
.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('end_YGOPro_single.html', data, 202, 222);
}
});
} catch (error) {
console.log(error);
}
};
create_room(room: Room) {
let options_buffer = new Buffer(6);
// 建主密码 https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit
options_buffer.writeUInt8((room.private ? 2 : 1) << 4, 1);
options_buffer.writeUInt8(
room.options.rule << 5 |
room.options.mode << 3 |
(room.options.enable_priority ? 1 << 2 : 0) |
(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.servers[0]);
}
copy(text: string, event: Event) {
clipboard.writeText(text);
$('#copy-wrapper').tooltip({ trigger: 'manual' }).tooltip('show');
}
join_room(room: Room) {
let options_buffer = new Buffer(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 = new Buffer(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.servers[0]);
}
request_match(arena = 'entertain') {
let headers = new Headers();
headers.append('Authorization',
'Basic ' + Buffer.from(this.loginService.user.username + ':' + this.loginService.user.external_id).toString('base64'));
match_started_at = new Date();
this.matching_arena = matching_arena = arena;
this.matching = matching = this.http.post('https://api.mycard.moe/ygopro/match', null, {
headers: headers,
search: {
arena,
locale: this.settingsService.getLocale()
}
}).map(response => response.json())
.subscribe((data) => {
this.join(data['password'], { 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: any) {
// 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);
// }
}
This diff was suppressed by a .gitattributes entry.
export const environment = {
production: true
};
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.
export const environment = {
production: false
};
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MyCard</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<mycard-root>
<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 class="loading">
<img src="assets/CubbitLogo.png">
<p>
LOADING
<span>.</span>
<span>.</span>
<span>.</span>
</p>
</div>
</mycard-root>
<script>
document.body.classList.add(process.platform);
</script>
</body>
</html>
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { MyCardModule } from './app/mycard.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(MyCardModule);
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
/**
* Required to support Web Animations `@angular/animation`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.
/**
* Need to import at least one locale-data with intl.
*/
// import 'intl/locale-data/jsonp/en';
/* You can add global styles to this file, and also import other style files */
@import "~bootstrap/dist/css/bootstrap.css";
@import "~font-awesome/css/font-awesome.css";
html, body { html, body {
height: 100%; height: 100%;
/*overflow: hidden;*/ /*overflow: hidden;*/
...@@ -15,7 +19,7 @@ body { ...@@ -15,7 +19,7 @@ body {
/*padding-right: 0 !important;*/ /*padding-right: 0 !important;*/
/*}*/ /*}*/
mycard { mycard-root {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
......
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "es2015",
"types": [
"node"
]
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}
/* SystemJS module definition */
declare var module: NodeModule;
interface NodeModule {
id: string;
}
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// systemjs plugins
'text': 'npm:systemjs-plugin-text/text.js',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
// node.js built-in libraries
"buffer": "@node/buffer",
"querystring": "@node/querystring",
"events": "@node/events",
"http": "@node/http",
"cluster": "@node/cluster",
"zlib": "@node/zlib",
"os": "@node/os",
"https": "@node/https",
"punycode": "@node/punycode",
"repl": "@node/repl",
"readline": "@node/readline",
"vm": "@node/vm",
"child_process": "@node/child_process",
"url": "@node/url",
"dns": "@node/dns",
"net": "@node/net",
"glob": "@node/glob",
"dgram": "@node/dgram",
"fs": "@node/fs",
"path": "@node/path",
"string_decoder": "@node/string_decoder",
"tls": "@node/tls",
"crypto": "@node/crypto",
"stream": "@node/stream",
"util": "@node/util",
"assert": "@node/assert",
"tty": "@node/tty",
"domain": "@node/domain",
"constants": "@node/constants",
"process": "@node/process",
"v8": "@node/v8",
"timers": "@node/timers",
"console": "@node/console",
// other node.js libraries
"electron": "@node/electron",
"ini": "@node/ini",
"mkdirp": "@node/mkdirp",
"aria2": "@node/aria2",
"electron-sudo": "@node/electron-sudo",
'fs-extra': '@node/fs-extra',
'jquery': 'npm:jquery/dist/jquery.min.js',
'tether': 'npm:tether/dist/js/tether.min.js',
'bootstrap': 'npm:bootstrap/dist/js/bootstrap.min.js',
'reconnecting-websocket': 'npm:reconnecting-websocket/dist/index.js',
// 'typeahead.js': '@node/typeahead.js'
'raven-js': 'npm:raven-js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
},
'raven-js': {
main: 'dist/raven.js'
}
},
meta: {
bootstrap: {
globals: {
jQuery: 'jquery',
Tether: 'tether'
}
}
}
});
{
"compilerOptions": {
"target": "es6",
"module": "es2015",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2017",
"dom"
],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true,
"strictNullChecks": true,
"skipLibCheck": true
},
"files": [
"app/mycard.module.ts",
"app/main.ts"
],
"angularCompilerOptions": {
"genDir": "aot",
"skipMetadataEmit" : true
}
}
{ {
"compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"target": "es6", "outDir": "./dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true, "sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [ "lib": [
"es2017", "esnext",
"dom.iterable" "dom.iterable"
], ],
"suppressImplicitAnyIndexErrors": true, "downlevelIteration": true
"strict": true, }
"skipLibCheck": true
},
"exclude": [
"node_modules/*",
"**/*-aot.ts"
]
} }
{ {
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": { "rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true, "class-name": true,
"comment-format": [ "comment-format": [
true, true,
...@@ -8,12 +13,17 @@ ...@@ -8,12 +13,17 @@
"curly": true, "curly": true,
"eofline": true, "eofline": true,
"forin": true, "forin": true,
"import-blacklist": [
true,
"rxjs"
],
"import-spacing": true,
"indent": [ "indent": [
true, true,
"spaces" "spaces"
], ],
"interface-over-type-literal": true,
"label-position": true, "label-position": true,
"label-undefined": true,
"max-line-length": [ "max-line-length": [
true, true,
140 140
...@@ -21,10 +31,17 @@ ...@@ -21,10 +31,17 @@
"member-access": false, "member-access": false,
"member-ordering": [ "member-ordering": [
true, true,
"static-before-instance", {
"variables-before-functions" "order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
], ],
"no-arg": true, "no-arg": true,
"no-bitwise": true,
"no-console": [ "no-console": [
true, true,
"debug", "debug",
...@@ -35,18 +52,23 @@ ...@@ -35,18 +52,23 @@
], ],
"no-construct": true, "no-construct": true,
"no-debugger": true, "no-debugger": true,
"no-duplicate-key": true, "no-duplicate-super": true,
"no-duplicate-variable": true,
"no-empty": false, "no-empty": false,
"no-empty-interface": true,
"no-eval": true, "no-eval": true,
"no-inferrable-types": true, "no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": false,
"no-shadowed-variable": true, "no-shadowed-variable": true,
"no-string-literal": false, "no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true, "no-switch-case-fall-through": true,
"no-trailing-whitespace": true, "no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true, "no-unused-expression": true,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": true, "no-use-before-declare": true,
"no-var-keyword": true, "no-var-keyword": true,
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
...@@ -57,11 +79,14 @@ ...@@ -57,11 +79,14 @@
"check-else", "check-else",
"check-whitespace" "check-whitespace"
], ],
"prefer-const": true,
"quotemark": [ "quotemark": [
true, true,
"single" "single"
], ],
"radix": true,
"semicolon": [ "semicolon": [
true,
"always" "always"
], ],
"triple-equals": [ "triple-equals": [
...@@ -78,6 +103,8 @@ ...@@ -78,6 +103,8 @@
"variable-declaration": "nospace" "variable-declaration": "nospace"
} }
], ],
"typeof-compare": true,
"unified-signatures": true,
"variable-name": false, "variable-name": false,
"whitespace": [ "whitespace": [
true, true,
...@@ -86,6 +113,30 @@ ...@@ -86,6 +113,30 @@
"check-operator", "check-operator",
"check-separator", "check-separator",
"check-type" "check-type"
] ],
"directive-selector": [
true,
"attribute",
"mycard",
"camelCase"
],
"component-selector": [
true,
"element",
"mycard",
"kebab-case"
],
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"no-access-missing-member": true,
"templates-use-public": true,
"invoke-injectable": true
} }
} }
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
<title id="title"></title> <title id="title"></title>
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="node_modules/vue/dist/vue.min.js"></script> <script src="../node_modules/vue/dist/vue.min.js"></script>
<style> <style>
h1 { h1 {
margin-top: 10px; margin-top: 10px;
......
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