Commit 631f4741 authored by 神楽坂玲奈's avatar 神楽坂玲奈

init

parents
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "mycard-mobile"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"serviceWorker": true,
"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"
},
{
"project": "src/tsconfig.spec.json"
},
{
"project": "e2e/tsconfig.e2e.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"class": {
"spec": false
},
"component": {
"spec": false
},
"directive": {
"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
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/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
Thumbs.db
This diff is collapsed.
{
"name": "mycard-mobile",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve --base-href /mobile/index.html --deploy-url /mobile --locale zh-CN --output-path mobile --open",
"build": "ng build --base-href /mobile/index.html --locale zh-CN --aot --prod",
"test": "echo \"Error: no test... minimal project\" && exit 1",
"lint": "echo \"Error: no lint... minimal project\" && exit 1",
"e2e": "echo \"Error: no e2e... minimal project\" && exit 1"
},
"private": true,
"dependencies": {
"@angular/animations": "^4.3.0",
"@angular/cdk": "^2.0.0-beta.8",
"@angular/common": "^4.0.0",
"@angular/compiler": "^4.0.0",
"@angular/core": "^4.0.0",
"@angular/forms": "^4.0.0",
"@angular/http": "^4.0.0",
"@angular/material": "^2.0.0-beta.8",
"@angular/platform-browser": "^4.0.0",
"@angular/platform-browser-dynamic": "^4.0.0",
"@angular/router": "^4.0.0",
"core-js": "^2.4.1",
"font-awesome": "^4.7.0",
"hammerjs": "^2.0.8",
"lodash": "^4.17.4",
"material-design-icons": "^3.0.1",
"rxjs": "^5.1.0",
"url-polyfill": "^1.0.6",
"zone.js": "^0.8.4"
},
"devDependencies": {
"@angular/cli": "1.2.1",
"@angular/compiler-cli": "^4.0.0",
"@angular/language-service": "^4.0.0",
"@angular/service-worker": "^1.0.0-beta.16",
"@types/node": "^8.0.13",
"codelyzer": "^3.1.2",
"tslint": "^5.5.0",
"typescript": "~2.3.3"
}
}
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { LobbyComponent } from './lobby/lobby.component';
import { LoginService } from './login.service';
import { MatchDialog } from './match/match.component';
import { NewRoomComponent } from './new-room/new-room.component';
import { RoomListComponent } from './room-list/room-list.component';
import { WindbotComponent } from './windbot/windbot.component';
import { WatchComponent } from './watch/watch.component';
const routes: Routes = [
{
path: '',
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: '/ygopro/lobby', pathMatch: 'full' },
{ path: 'ygopro/rooms/new', component: NewRoomComponent },
{ path: 'ygopro/rooms', component: RoomListComponent },
{ path: 'ygopro/lobby', component: LobbyComponent },
{ path: 'ygopro/match/:arena', component: MatchDialog },
{ path: 'ygopro/windbot', component: WindbotComponent },
{ path: 'ygopro/watch', component: WatchComponent },
// { path: 'ygopro/rooms/join', component: JoinComponent }
]
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule],
providers: [AuthGuard, LoginService],
})
export class AppRoutingModule {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<!--<h1>-->
<!--Welcome to {{title}}!!-->
<!--</h1>-->
<router-outlet></router-outlet>
`,
styles: []
})
export class AppComponent {
title = 'app';
}
import { CdkTableModule } from '@angular/cdk';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import {
MdAutocompleteModule,
MdButtonModule,
MdCardModule,
MdCheckboxModule,
MdDialogModule,
MdGridListModule,
MdIconModule,
MdInputModule,
MdListModule, MdMenuModule,
MdSelectModule,
MdSlideToggleModule,
MdSnackBarModule,
MdTableModule,
MdToolbarModule
} from '@angular/material';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import 'hammerjs';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LobbyComponent } from './lobby/lobby.component';
import { MatchDialog } from './match/match.component';
import { NewRoomComponent } from './new-room/new-room.component';
import { RoomListComponent } from './room-list/room-list.component';
import { ToolbarComponent } from './toolbar/toolbar.component';
import { WatchComponent } from './watch/watch.component';
import { WindbotComponent } from './windbot/windbot.component';
import { YGOProService } from './ygopro.service';
@NgModule({
declarations: [
AppComponent,
LobbyComponent,
NewRoomComponent,
RoomListComponent,
MatchDialog,
WindbotComponent,
WatchComponent,
ToolbarComponent,
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule,
BrowserAnimationsModule,
MdInputModule,
MdSelectModule,
MdCheckboxModule,
MdButtonModule,
MdSlideToggleModule,
MdCardModule,
MdGridListModule,
MdIconModule,
MdTableModule,
CdkTableModule,
MdListModule,
MdDialogModule,
MdToolbarModule,
MdSnackBarModule,
MdAutocompleteModule,
ReactiveFormsModule,
JsonpModule,
MdMenuModule
],
providers: [YGOProService],
bootstrap: [AppComponent],
entryComponents: [MatchDialog],
})
export class AppModule {
}
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { LoginService } from './login.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private login: LoginService) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const token = state.root.queryParamMap.get('sso') || new URL(location.href).searchParams.get('sso') || localStorage.getItem('login');
console.log(token)
if (!token) {
alert('login required');
return false;
}
this.login.sso(token);
return true;
}
}
#menu {
margin-left: -8px;
margin-right: 24px;
}
#avatar {
width: 24px;
height: 24px;
border-radius: 50%;
}
form {
flex: 1 1 auto;
}
input {
background: none;
border: none;
font-size: 20px;
color: white;
width: 100%;
padding-right: 8px;
}
input:focus {
outline: none;
}
input::placeholder {
color: lightgray;
}
/*not works*/
/*input::-webkit-search-cancel-button {*/
/*color: lightgray;*/
/*}*/
md-input-container {
width: 100%;
}
[md-raised-button] {
border-radius: 0;
box-shadow: none;
}
[md-button], [md-raised-button] {
width: 100%;
height: 100%;
}
a[md-button], a[md-raised-button] {
display: flex;
align-items: center;
justify-content: center;
}
md-card-content {
display: flex;
flex-direction: row;
}
[md-card-avatar] {
width: 128px;
height: 72px;
background-size: cover;
border-radius: initial;
}
#avatar {
background-size: cover;
}
[fontSet] {
font-size: 24px;
}
.icon {
font-size: 20px;
}
#avatar /deep/ .mat-button-wrapper {
background: red;
position: absolute;
left: 0;
right: 0;
bottom: 0;
line-height: 24px;
}
a {
color: inherit;
text-decoration: inherit;
}
<md-toolbar color="primary">
<button id="menu" md-icon-button>
<md-icon>menu</md-icon>
</button>
<form (submit)="search(key)">
<input type=search placeholder="卡片搜索" name="key" [(ngModel)]="key" [mdAutocomplete]="auto" [formControl]="searchCtrl">
<md-autocomplete #auto="mdAutocomplete">
<md-option *ngFor="let card of suggestion | async" [value]="card" (onSelectionChange)="search(card)">
{{ card }}
</md-option>
</md-autocomplete>
</form>
<button md-icon-button>
<img id="avatar" [src]="login.user.avatar_url">
</button>
<button md-icon-button [mdMenuTriggerFor]="menu">
<md-icon>add</md-icon>
</button>
<md-menu #menu="mdMenu">
<button md-menu-item>Item 1</button>
<button md-menu-item>Item 2</button>
</md-menu>
</md-toolbar>
<md-grid-list cols="4" rowHeight="100px" gutterSize="0">
<!--<md-grid-tile>-->
<!--<a href="https://accounts.moecube.com/profiles" id="avatar" md-raised-button [style.background-image]="'url(' + login.user.avatar_url + ')'">{{login.user.username}}</a>-->
<!--</md-grid-tile>-->
<md-grid-tile>
<button md-raised-button color="primary" (click)="request_match('athletic')">
<md-icon fontSet="fa" fontIcon="fa-futbol-o"></md-icon>
<br>竞技匹配
</button>
</md-grid-tile>
<md-grid-tile>
<button md-raised-button color="primary" (click)="request_match('entertain')">
<md-icon>toys</md-icon>
<br>娱乐匹配
</button>
</md-grid-tile>
<md-grid-tile><a md-raised-button color="primary" routerLink="/ygopro/rooms">
<md-icon>games</md-icon>
<br>房间列表</a></md-grid-tile>
<md-grid-tile><a md-raised-button color="primary" routerLink="/ygopro/rooms/new">
<md-icon>add_box</md-icon>
<br>创建房间</a></md-grid-tile>
<md-grid-tile><a md-raised-button routerLink="/ygopro/windbot">
<md-icon>airplanemode_active</md-icon>
<br>单人模式</a></md-grid-tile>
<md-grid-tile><a md-raised-button routerLink="/ygopro/watch">
<md-icon>remove_red_eye</md-icon>
<br>观战</a></md-grid-tile>
<md-grid-tile>
<button md-raised-button (click)="ygopro.watch_replay()">
<md-icon>history</md-icon>
<br>观看录像
</button>
</md-grid-tile>
<md-grid-tile>
<button md-raised-button (click)="ygopro.edit_deck()">
<md-icon>edit</md-icon>
<br>编辑卡组
</button>
</md-grid-tile>
<md-grid-tile><a md-raised-button href="https://ygobbs.com" target="_blank">
<md-icon>forum</md-icon>
<br>社区</a></md-grid-tile>
<md-grid-tile><a md-raised-button [href]="arena_url" target="_blank">
<md-icon fontSet="fa" fontIcon="fa-trophy"></md-icon>
<br>决斗数据库</a></md-grid-tile>
<md-grid-tile>
<button md-raised-button><span class="icon">233</span><br>直连</button>
</md-grid-tile>
</md-grid-list>
<md-card *ngFor="let item of ygopro.news" class="example-card">
<a [href]="item.url" target="_blank">
<md-card-header>
<div *ngIf="item.image" md-card-avatar [style.background-image]="'url(' + item.image + ')'"></div>
<md-card-title>{{item.title}}</md-card-title>
<md-card-subtitle>{{item.updated_at | date:"mediumDate"}}</md-card-subtitle>
</md-card-header>
</a>
<!--<md-card-content>-->
<!--<img src="https://material.angular.io/assets/img/examples/shiba2.jpg">-->
<!--<p>{{item.text}}</p>-->
<!--</md-card-content>-->
</md-card>
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Http, Jsonp } from '@angular/http';
import { MdDialog } from '@angular/material';
import { ActivatedRoute } from '@angular/router';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';
import { LoginService } from '../login.service';
import { MatchDialog } from '../match/match.component';
import { YGOProService } from '../ygopro.service';
@Component({
selector: 'app-lobby',
templateUrl: 'lobby.component.html',
styleUrls: ['lobby.component.css']
})
export class LobbyComponent {
searchCtrl: FormControl;
suggestion: any;
key: string;
arena_url: string;
constructor(public login: LoginService, public ygopro: YGOProService, public dialog: MdDialog, private http: Http, private jsonp: Jsonp, private route: ActivatedRoute) {
this.searchCtrl = new FormControl();
this.suggestion = this.searchCtrl.valueChanges.flatMap(name => name ? this.jsonp.get('http://www.ourocg.cn/Suggest.aspx', {
params: { callback: 'JSONP_CALLBACK', key: name }
}).map(response => response.json().result) : []);
// this.jsonp.get('http://www.ourocg.cn/Suggest.aspx', {
// params: {
// callback: 'JSONP_CALLBACK',
// key: 'xy'
// }
// }).map(response => response.json().result).toPromise().then(data => console.log(data));
const arena_url = new URL('https://mycard.moe/ygopro/arena');
arena_url.searchParams.set('sso', login.token);
this.arena_url = arena_url.toString();
}
async request_match(arena: string) {
this.dialog.open(MatchDialog, { data: arena, disableClose: true });
}
search(key) {
const url = new URL('http://www.ourocg.cn/S.aspx');
url.searchParams.set('key', key);
open(url.toString());
}
}
import { Injectable } from '@angular/core';
import { User } from './ygopro.service';
@Injectable()
export class LoginService {
user: User;
token;
sso(token: string) {
this.token = token;
let user = <User>{};
for (let [key, value] of new URLSearchParams(Buffer.from(token, 'base64').toString())) {
user[key] = value;
}
this.user = user;
localStorage.setItem('login', token);
}
}
md-icon {
width: 96px;
height: 96px;
font-size: 96px;
}
md-dialog-content {
display: flex;
}
md-dialog-actions {
justify-content: flex-end
}
ul {
margin: 0;
}
<h2 md-dialog-title *ngIf="arena == 'athletic'">竞技匹配</h2>
<h2 md-dialog-title *ngIf="arena == 'entertain'">娱乐匹配</h2>
<md-dialog-content>
<md-icon *ngIf="arena == 'athletic'" fontSet="fa" fontIcon="fa-futbol-o" class="fa-spin"></md-icon>
<md-icon *ngIf="arena == 'entertain'" class="fa-spin">toys</md-icon>
<ul>
<dt>预计等待时间</dt>
<dd>{{expect_wait | date: 'mm:ss'}}</dd>
<dt>实际等待时间</dt>
<dd>{{actual_wait | async | date: 'mm:ss'}}</dd>
</ul>
</md-dialog-content>
<md-dialog-actions>
<button md-button md-dialog-close>取消</button>
</md-dialog-actions>
import { Component, Inject } from '@angular/core';
import { MD_DIALOG_DATA } from '@angular/material';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-match',
templateUrl: './match.component.html',
styleUrls: ['./match.component.css']
})
export class MatchDialog {
expect_wait = new Date(1);
actual_wait = Observable.timer(0, 1000).map(timestamp => new Date(timestamp * 1000));
constructor(@Inject(MD_DIALOG_DATA) public arena: string) {
}
}
form {
padding: 8px;
}
.full-width {
display: block;
width: 100%;
}
/*.example-margin {*/
/*margin: 0 10px;*/
/*}*/
md-select {
padding: 1.2734375em 0;
}
#actions {
text-align: right;
}
<app-toolbar>创建房间</app-toolbar>
<form (submit)="ygopro.create_room(room, host_password)">
<md-input-container *ngIf="!room.private" class="full-width">
<input mdInput placeholder="游戏标题" name="title" [(ngModel)]="room.title">
<md-hint align="end">{{room.title?.length || 0}} / 12</md-hint>
</md-input-container>
<md-input-container *ngIf="room.private" class="full-width">
<md-placeholder><md-icon>vpn_key</md-icon><span>房间密码</span></md-placeholder>
<input #hostPasswordRef mdInput name="title" [(ngModel)]="host_password" readonly>
<button type="button" md-icon-button mdSuffix (click)="copy(host_password)"><md-icon>content_copy</md-icon></button>
<button type="button" md-icon-button mdSuffix (click)="share(host_password)"><md-icon>share</md-icon></button>
<md-hint align="end">把这个分享给你的朋友</md-hint>
</md-input-container>
<md-select class="full-width" placeholder="卡片允许" name="rule" [(ngModel)]="room.options.rule">
<md-option value="0">OCG</md-option>
<md-option value="1">TCG</md-option>
<md-option value="2">OCG & TCG</md-option>
<md-option value="3">专有卡禁止</md-option>
</md-select>
<md-select class="full-width" placeholder="决斗模式" name="mode" [(ngModel)]="room.options.mode">
<md-option value="0">单局模式</md-option>
<md-option value="1">比赛模式</md-option>
<md-option value="2">TAG</md-option>
</md-select>
<!--<h2>额外选项</h2>-->
<!--<md-slide-toggle #extra class="example-margin" color="primary">额外选项</md-slide-toggle>-->
<md-input-container class="full-width">
<input name="start_lp" [(ngModel)]="room.options.start_lp" mdInput type="number" min="1" max="65536" placeholder="初始 LP">
</md-input-container>
<md-input-container class="full-width">
<input name="start_hand" [(ngModel)]="room.options.start_hand" mdInput type="number" min="0" max="16" placeholder="初始手牌数">
</md-input-container>
<md-input-container class="full-width">
<input name="draw_count" [(ngModel)]="room.options.draw_count" mdInput type="number" min="0" max="16" placeholder="每回合抽卡">
</md-input-container>
<md-checkbox class="full-width" name="room.private" [(ngModel)]="room.private">私密房间</md-checkbox>
<md-checkbox class="full-width" name="room.enable_priority" [(ngModel)]="room.options.enable_priority">旧规则
</md-checkbox>
<md-checkbox class="full-width" name="room.no_check_deck" [(ngModel)]="room.options.no_check_deck">不检查卡组</md-checkbox>
<md-checkbox class="full-width" name="room.no_shuffle_deck" [(ngModel)]="room.options.no_shuffle_deck">不洗切卡组
</md-checkbox>
<div id="actions">
<!--<button routerLink="/ygopro/lobby" md-raised-button>返回大厅</button>-->
<button type="submit" color="primary" md-raised-button>创建游戏</button>
</div>
</form>
import { Component, ElementRef, ViewChild } from '@angular/core';
import { MdSnackBar } from '@angular/material';
import { LoginService } from '../login.service';
import { default_options, YGOProService } from '../ygopro.service';
@Component({
selector: 'app-new-room',
templateUrl: 'new-room.component.html',
styleUrls: ['new-room.component.css']
})
export class NewRoomComponent {
@ViewChild('hostPasswordRef')
hostPasswordRef: ElementRef;
host_password = (this.login.user.external_id ^ 0x54321).toString();
room = {
title: this.login.user.username + '的房间',
'private': false,
options: { ...default_options }
};
constructor(public ygopro: YGOProService, private login: LoginService, private snackBar: MdSnackBar) {
}
copy(host_password: string) {
try {
this.hostPasswordRef.nativeElement.select();
if (document.execCommand('copy')) {
this.snackBar.open(`房间密码 ${host_password} 已复制到剪贴板`, null, { duration: 3000 });
} else {
console.log('Oops, unable to copy');
}
} catch (error) {
console.log(error);
}
}
share(host_password: string) {
}
}
.avatar {
width: 1em;
height: 1em;
}
.game-title {
flex: 2
}
:host {
display: flex;
flex-direction: column;
}
md-table {
flex: 1;
padding-bottom: 36px;
}
form {
display: flex;
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
input {
flex: 1;
padding-left: 8px;
padding-right: 8px;
/*border-top: 1px solid rgba(0,0,0,.12)*/
}
<app-toolbar>房间列表</app-toolbar>
<md-table #table [dataSource]="dataSource">
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on room row definition" -->
<!-- ID Column -->
<ng-container cdkColumnDef="title">
<md-header-cell class="game-title" *cdkHeaderCellDef>游戏标题</md-header-cell>
<md-cell class="game-title" *cdkCellDef="let room">{{room.title}}</md-cell>
</ng-container>
<!-- Progress Column -->
<ng-container cdkColumnDef="users">
<md-header-cell *cdkHeaderCellDef>玩家</md-header-cell>
<md-cell *cdkCellDef="let room">
<img *ngFor="let user of room.users" class="avatar" [src]="'https://ygobbs.com/user_avatar/ygobbs.com/' + user.username + '/25/1.png'">
</md-cell>
</ng-container>
<!-- Name Column -->
<ng-container cdkColumnDef="mode">
<md-header-cell *cdkHeaderCellDef>决斗模式</md-header-cell>
<md-cell *cdkCellDef="let room">
<span *ngIf="room.options.mode === 0">单局模式</span>
<span *ngIf="room.options.mode === 1">比赛模式</span>
<span *ngIf="room.options.mode === 2">TAG</span>
</md-cell>
</ng-container>
<!-- Color Column -->
<ng-container cdkColumnDef="extra">
<md-header-cell *cdkHeaderCellDef>额外选项</md-header-cell>
<md-cell *cdkCellDef="let room">
<span *ngIf="room.options.rule != default_options.rule">{{{'0': 'OCG', '1': 'TCG', '2': 'O/T', '3': '专有卡禁止'}[room.options.rule]}}</span>
<span *ngIf="room.options.start_lp != default_options.start_lp">{{room.options.start_lp}} LP</span>
<span *ngIf="room.options.start_hand != default_options.start_hand">{{room.options.start_hand}} 初始</span>
<span *ngIf="room.options.draw_count != default_options.draw_count">{{room.options.draw_count}} 抽卡</span>
<span *ngIf="room.options.enable_priority != default_options.enable_priority">旧规则</span>
<span *ngIf="room.options.no_check_deck != default_options.no_check_deck">不检查</span>
<span *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck">不洗卡</span>
</md-cell>
</ng-container>
<md-header-row *cdkHeaderRowDef="displayedColumns"></md-header-row>
<md-row *cdkRowDef="let room; columns: displayedColumns;" (click)="ygopro.join_room(room)"></md-row>
</md-table>
<form class="example-form">
<input placeholder="在这输入你朋友的私密房间密码就可以进去了哦!">
<button md-raised-button color="primary">加入私密房间</button>
</form>
import { ChangeDetectorRef, Component } from '@angular/core';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/startWith';
import 'rxjs/Rx';
import { default_options, RoomListDataSource, servers, YGOProService } from '../ygopro.service';
@Component({
selector: 'app-room-list',
styleUrls: ['room-list.component.css'],
templateUrl: 'room-list.component.html',
})
export class RoomListComponent {
displayedColumns = ['title', 'users', 'mode', 'extra'];
dataSource = new RoomListDataSource(servers.filter(server => server.custom));
default_options = default_options;
constructor(public ygopro: YGOProService, private changeDetector: ChangeDetectorRef) {
}
ngOnInit() {
this.changeDetector.detectChanges();
}
}
button {
margin-left: -8px;
margin-right: 24px;
}
<md-toolbar color="primary">
<button md-icon-button (click)="back()"><md-icon>arrow_back</md-icon></button>
<span><ng-content></ng-content></span>
</md-toolbar>
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.css']
})
export class ToolbarComponent {
constructor() { }
back() {
history.back();
}
}
.avatar {
width: 1em;
height: 1em;
}
.game-title {
flex: 2
}
<app-toolbar>观战</app-toolbar>
<md-table #table [dataSource]="dataSource">
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on room row definition" -->
<!-- ID Column -->
<ng-container cdkColumnDef="mode">
<md-header-cell *cdkHeaderCellDef>游戏模式</md-header-cell>
<md-cell *cdkCellDef="let room">
<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>
</md-cell>
</ng-container>
<!-- ID Column -->
<ng-container cdkColumnDef="title">
<md-header-cell class="game-title" *cdkHeaderCellDef>游戏标题</md-header-cell>
<md-cell class="game-title" *cdkCellDef="let room">
<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}} Vs. {{room.users[1] && room.users[1].username}}</span>
<span *ngIf="!(room.arena || room.id.startsWith('AI#') || room.private)">{{room.title}}</span>
</md-cell>
</ng-container>
<!-- Progress Column -->
<ng-container cdkColumnDef="users">
<md-header-cell *cdkHeaderCellDef>玩家</md-header-cell>
<md-cell *cdkCellDef="let room">
<img *ngFor="let user of room.users" class="avatar" [src]="'https://ygobbs.com/user_avatar/ygobbs.com/' + user.username + '/25/1.png'">
</md-cell>
</ng-container>
<!-- Color Column -->
<ng-container cdkColumnDef="extra">
<md-header-cell *cdkHeaderCellDef>额外选项</md-header-cell>
<md-cell *cdkCellDef="let room">
<span *ngIf="room.options.rule != default_options.rule">{{{'0': 'OCG', '1': 'TCG', '2': 'O/T', '3': '专有卡禁止'}[room.options.rule]}}</span>
<span *ngIf="room.options.start_lp != default_options.start_lp">{{room.options.start_lp}} LP</span>
<span *ngIf="room.options.start_hand != default_options.start_hand">{{room.options.start_hand}} 初始</span>
<span *ngIf="room.options.draw_count != default_options.draw_count">{{room.options.draw_count}} 抽卡</span>
<span *ngIf="room.options.enable_priority != default_options.enable_priority">旧规则</span>
<span *ngIf="room.options.no_check_deck != default_options.no_check_deck">不检查</span>
<span *ngIf="room.options.no_shuffle_deck != default_options.no_shuffle_deck">不洗卡</span>
</md-cell>
</ng-container>
<md-header-row *cdkHeaderRowDef="displayedColumns"></md-header-row>
<md-row *cdkRowDef="let room; columns: displayedColumns;" (click)="ygopro.join_room(room)"></md-row>
</md-table>
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { default_options, RoomListDataSource, servers, YGOProService } from '../ygopro.service';
@Component({
selector: 'app-watch',
templateUrl: './watch.component.html',
styleUrls: ['./watch.component.css']
})
export class WatchComponent implements OnInit {
displayedColumns = ['mode', 'title', 'users', 'extra'];
dataSource = new RoomListDataSource(servers, 'started');
default_options = default_options;
constructor(public ygopro: YGOProService, private changeDetector: ChangeDetectorRef) {
}
ngOnInit() {
this.changeDetector.detectChanges();
}
}
<app-toolbar>单人模式</app-toolbar>
<md-list>
<h3 md-subheader>选择对手</h3>
<md-list-item (click)="ygopro.join_windbot()">随机</md-list-item>
<md-list-item *ngFor="let windbot of ygopro.windbot" (click)="ygopro.join_windbot(windbot)">{{windbot}}</md-list-item>
</md-list>
import { Component, OnInit } from '@angular/core';
import { YGOProService } from '../ygopro.service';
@Component({
selector: 'app-windbot',
templateUrl: './windbot.component.html',
styleUrls: ['./windbot.component.css']
})
export class WindbotComponent {
constructor(public ygopro: YGOProService) {
}
}
import { DataSource } from '@angular/cdk';
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { sortBy } from 'lodash';
import { Observable } from 'rxjs/Observable';
import { LoginService } from './login.service';
export interface User {
admin: boolean;
avatar_url: string;
email: string;
external_id: number;
moderator: boolean;
name: string;
username: string;
}
export interface Room {
id?: string;
title?: string;
server?: Server;
'private'?: boolean;
options: Options;
arena?: string;
users?: { username: string, position: number }[];
}
export 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 Server {
id?: string;
url?: string;
address: string;
port: number;
custom?: boolean;
replay?: boolean;
}
export const servers: Server[] = [{
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
}];
export const default_options: Options = {
mode: 1,
rule: 0,
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
};
class News {
title: string;
text: string;
url: string;
image: string;
updated_at: Date;
}
interface YGOProData {
windbot: { [locale: string]: string[] };
}
interface App {
id: string,
news: { [locale: string]: News[] },
windbot: { [locale: string]: string[] },
data: any
}
@Injectable()
export class YGOProService {
news: News[];
windbot: string[];
constructor(private login: LoginService, private http: Http) {
this.load().catch(alert);
}
async load() {
const apps: App[] = await this.http.get('https://api.mycard.moe/apps.json').map(response => response.json()).toPromise();
const app = apps.find(app => app.id === 'ygopro');
this.news = app.news['zh-CN'];
this.windbot = (<YGOProData>app.data).windbot['zh-CN'];
}
create_room(room: Room, host_password: string) {
let options_buffer = Buffer.alloc(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.login.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 ? 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, servers[0]);
}
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.login.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.login.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, servers[0]);
}
join_windbot(name?: string) {
if (!name) {
name = this.windbot[Math.floor(Math.random() * this.windbot.length)];
}
return this.join('AI#' + name, servers[0]);
}
join(password, server) {
try {
window.ygopro.join(server.address, server.port, this.login.user.username, password);
} catch (error) {
console.error(error);
alert(JSON.stringify({
method: 'join',
params: [server.address, server.port, this.login.user.username, password]
}));
}
}
edit_deck() {
try {
window.ygopro.edit_deck();
} catch (error) {
console.error(error);
alert(JSON.stringify({ method: 'edit_deck', params: [] }));
}
}
watch_replay() {
try {
window.ygopro.watch_replay();
} catch (error) {
console.error(error);
alert(JSON.stringify({ method: 'watch_replay', params: [] }));
}
}
single_mode() {
try {
window.ygopro.puzzle_mode();
} catch (error) {
console.error(error);
alert(JSON.stringify({ method: 'puzzle_mode', params: [] }));
}
}
}
type Message =
{ event: 'init', data: Room[] }
| { event: 'update', data: Room }
| { event: 'create', data: Room }
| { event: 'delete', data: string }
export class RoomListDataSource extends DataSource<any> {
constructor(private servers: Server[], private filter = 'waiting') {
super();
}
/** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<Room[]> {
return Observable.combineLatest(servers.map(server => {
const url = new URL(server.url);
url.searchParams.set('filter', this.filter);
return Observable.webSocket({ url: url.toString() })
.scan((rooms: Room[], message: Message) => {
console.log(message);
switch (message.event) {
case 'init':
console.log(message.data.map(room => ({ server: server, ...room })));
return message.data.map(room => ({ server: server, ...room }));
case 'create':
return rooms.concat({ server: server, ...message.data });
case 'update':
Object.assign(rooms.find(room => room.id === message.data.id), message.data);
return rooms;
case 'delete':
return rooms.filter(room => room.id != message.data);
}
}, []);
}
), (...sources) => [].concat(...sources)).map(rooms => sortBy(rooms, (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;
}
})
);
}
disconnect() {
}
}
declare global {
interface Window {
ygopro: {
// 加入房间
join (address: string, port: number, username: string, password: string): void
// 编辑卡组
edit_deck (): void
// 观看录像,进入观看录像界面
watch_replay(): void
// 残局模式
puzzle_mode(): void
};
}
}
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="zh-CN">
<head>
<meta charset="utf-8">
<title>MyCard Mobile</title>
<base href="/mobile/index.html">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html, body {
height: 100%;
}
body {
margin: 0;
}
#loading {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#loading > img {
width: 300px;
}
#loading p {
position: relative;
left: 10px;
font-weight: 600;
font-size: 30px;
color: #4b99ea;
text-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
margin-top: 0;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<app-root>
<div id="loading">
<img src="assets/CubbitLogo.png">
<p>LOADING <span>.</span> <span>.</span> <span>.</span></p>
<span id="version"></span>
</div>
</app-root>
</body>
</html>
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
/**
* 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';
import 'url-polyfill';
/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
@import '~material-design-icons/iconfont/material-icons.css';
@import '~font-awesome/css/font-awesome.min.css';
body {
margin: 0
}
{
"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;
}
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2016",
"dom.iterable"
]
}
}
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"typeof-compare": true,
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"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
}
}
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