Commit 505d8450 authored by 神楽坂玲奈's avatar 神楽坂玲奈

sync

parent a06e52d7
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve --base-href /mobile/index.html --deploy-url /mobile --locale zh-CN --output-path mobile --open", "start": "ng serve --base-href /mobile/index.html --deploy-url /mobile --locale zh-CN --output-path mobile --open",
"start:wild": "npm run start -- --host 0.0.0.0 --public-host 192.168.1.131:4200",
"build": "ng build --base-href /mobile/index.html --locale zh-CN --aot --prod", "build": "ng build --base-href /mobile/index.html --locale zh-CN --aot --prod",
"postinstall": "./fuck.sh", "postinstall": "./fuck.sh",
"build:dev": "ng build --base-href /mobile2/index.html --locale zh-CN --aot", "build:dev": "ng build --base-href /mobile2/index.html --locale zh-CN --aot",
...@@ -12,7 +13,6 @@ ...@@ -12,7 +13,6 @@
"publish": "npm run build && ./ossutil cp -rf dist oss://mycard/mobile", "publish": "npm run build && ./ossutil cp -rf dist oss://mycard/mobile",
"publish:dev": "npm run build:dev && ./ossutil cp -rf dist oss://mycard/mobile2" "publish:dev": "npm run build:dev && ./ossutil cp -rf dist oss://mycard/mobile2"
}, },
"private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^4.3.0", "@angular/animations": "^4.3.0",
"@angular/cdk": "^2.0.0-beta.8", "@angular/cdk": "^2.0.0-beta.8",
......
...@@ -12,7 +12,8 @@ import { ...@@ -12,7 +12,8 @@ import {
MdIconModule, MdIconModule,
MdInputModule, MdInputModule,
MdListModule, MdListModule,
MdMenuModule, MdProgressSpinnerModule, MdMenuModule,
MdProgressSpinnerModule,
MdSelectModule, MdSelectModule,
MdSlideToggleModule, MdSlideToggleModule,
MdSnackBarModule, MdSnackBarModule,
...@@ -31,13 +32,11 @@ import { MatchDialog } from './match/match.component'; ...@@ -31,13 +32,11 @@ import { MatchDialog } from './match/match.component';
import { NewRoomComponent } from './new-room/new-room.component'; import { NewRoomComponent } from './new-room/new-room.component';
import { ResultDialog } from './result/result.dialog'; import { ResultDialog } from './result/result.dialog';
import { RoomListComponent } from './room-list/room-list.component'; import { RoomListComponent } from './room-list/room-list.component';
import { StorageService } from './storage.service';
import { ToolbarComponent } from './toolbar/toolbar.component'; import { ToolbarComponent } from './toolbar/toolbar.component';
import { WatchComponent } from './watch/watch.component'; import { WatchComponent } from './watch/watch.component';
import { WindbotComponent } from './windbot/windbot.component'; import { WindbotComponent } from './windbot/windbot.component';
import { YGOProService } from './ygopro.service'; import { YGOProService } from './ygopro.service';
import { DecksComponent } from './decks/decks.component';
import { DragulaModule } from 'ng2-dragula';
import { SaveService } from './save.service';
@NgModule({ @NgModule({
declarations: [ declarations: [
...@@ -50,7 +49,6 @@ import { SaveService } from './save.service'; ...@@ -50,7 +49,6 @@ import { SaveService } from './save.service';
WatchComponent, WatchComponent,
ToolbarComponent, ToolbarComponent,
ResultDialog, ResultDialog,
DecksComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
...@@ -77,9 +75,8 @@ import { SaveService } from './save.service'; ...@@ -77,9 +75,8 @@ import { SaveService } from './save.service';
JsonpModule, JsonpModule,
MdMenuModule, MdMenuModule,
MdProgressSpinnerModule, MdProgressSpinnerModule,
DragulaModule
], ],
providers: [YGOProService, SaveService], providers: [YGOProService, StorageService],
bootstrap: [AppComponent], bootstrap: [AppComponent],
entryComponents: [MatchDialog, ResultDialog], entryComponents: [MatchDialog, ResultDialog],
}) })
......
...@@ -13,6 +13,9 @@ ...@@ -13,6 +13,9 @@
</md-option> </md-option>
</md-autocomplete> </md-autocomplete>
</form> </form>
<button md-icon-button *ngIf="storage.working">
<md-icon class="fa-spin">sync</md-icon>
</button>
<a href="https://accounts.moecube.com/profiles" target="_blank" md-icon-button> <a href="https://accounts.moecube.com/profiles" target="_blank" md-icon-button>
<img id="avatar" [src]="login.user.avatar_url"> <img id="avatar" [src]="login.user.avatar_url">
</a> </a>
......
...@@ -8,7 +8,7 @@ import { LoginService } from '../login.service'; ...@@ -8,7 +8,7 @@ import { LoginService } from '../login.service';
import { MatchDialog } from '../match/match.component'; import { MatchDialog } from '../match/match.component';
import { routerTransition2 } from '../router.animations'; import { routerTransition2 } from '../router.animations';
import { YGOProService } from '../ygopro.service'; import { YGOProService } from '../ygopro.service';
import { SaveService } from '../save.service'; import { StorageService } from '../storage.service';
@Component({ @Component({
selector: 'app-lobby', selector: 'app-lobby',
templateUrl: 'lobby.component.html', templateUrl: 'lobby.component.html',
...@@ -30,7 +30,7 @@ export class LobbyComponent { ...@@ -30,7 +30,7 @@ export class LobbyComponent {
arena_url: string; arena_url: string;
constructor(public login: LoginService, public ygopro: YGOProService, public dialog: MdDialog, private http: Http, private jsonp: Jsonp, private route: ActivatedRoute, public save: SaveService) { constructor(public login: LoginService, public ygopro: YGOProService, public dialog: MdDialog, private http: Http, private jsonp: Jsonp, private route: ActivatedRoute, public storage: StorageService) {
const arena_url = new URL('https://mycard.moe/ygopro/arena'); const arena_url = new URL('https://mycard.moe/ygopro/arena');
arena_url.searchParams.set('sso', login.token); arena_url.searchParams.set('sso', login.token);
......
import { Observable } from 'rxjs/Observable'; // import { Observable } from 'rxjs/Observable';
//
// import { bindCallback as staticBindCallback } from '../../observable/bindCallback'; // declare module 'rxjs/Observable' {
declare module 'rxjs/Observable' { // interface Observable<T> {
interface Observable<T> { // [Symbol.asyncIterator](): AsyncIterator<T>;
[Symbol.asyncIterator](): AsyncIterator<T>; // }
} // }
} //
// class Deferred<T> extends Promise<T> {
class Deferred<T> extends Promise<T> { // resolve;
resolve; // reject;
reject; //
// constructor() {
constructor() { // let a, b;
let a, b; // super((resolve, reject) => {
super((resolve, reject) => { // a = resolve;
a = resolve; // b = reject;
b = reject; // });
}); // this.resolve = a;
this.resolve = a; // this.reject = b;
this.reject = b; // }
} // }
} //
// Observable.prototype[Symbol.asyncIterator] = async function*() {
Observable.prototype[Symbol.asyncIterator] = async function*() { //
// let deferred = new Deferred();
let deferred = new Deferred(); // const completed = Symbol('completed');
const completed = Symbol('completed'); //
// this.subscribe({
this.subscribe({ // next(value){
next(value){ // deferred.resolve(value);
deferred.resolve(value); // },
}, // error(error){
error(error){ // deferred.reject(error);
deferred.reject(error); // },
}, // complete() {
complete() { // deferred.resolve(completed);
deferred.resolve(completed); // },
}, // });
}); //
// let value;
let value; // while ((value = await deferred) != completed) {
while ((value = await deferred) != completed) { // deferred = new Deferred();
deferred = new Deferred(); // yield value;
yield value; // }
} // };
};
import { Injectable } from '@angular/core';
import * as path from 'path';
import * as webdav from 'webdav';
import { LoginService } from './login.service';
import './o';
interface DirectoryStats {
'filename': string,
'basename': string,
'lastmod': string,
'size': 0,
'type': 'directory'
}
interface FileStats {
'filename': string,
'basename': string,
'lastmod': string,
'size': number,
'type': 'file',
'mime': string
}
type Stats = DirectoryStats | FileStats
@Injectable()
export class StorageService {
client = webdav('https://api.mycard.moe/storage/', this.login.user.username, this.login.user.external_id.toString());
working: boolean;
constructor(private login: LoginService) {
}
async sync(app_id: string) {
if (!window.ygopro.getFileLastModified) {
throw 'storage sync not supported';
}
console.log('sync', 'start');
this.working = true;
const root = path.join('/', app_id);
// 远程有 本地有
// 远程=本地 更新记录
// 远程>本地 下载
// 远程<本地 上传
// 远程有 本地无 记录无 下载
// 远程有 本地无 记录有 删除远端
// 远程无 本地有 记录无 上传
// 远程无 本地有 记录有 删除本地
// 远程无 本地无 记录有 更新记录
const remote_files = new Map<string, boolean>();
for await (const item of this.walk(root)) {
const remote_path = item.filename;
const local_path = path.relative(root, remote_path);
const index_path = '_FILE_' + remote_path;
const remote_time = Date.parse(item.lastmod);
remote_files.set(local_path, true);
const local_time = window.ygopro.getFileLastModified(local_path);
if (local_time) {
// 远端有,本地有
if (local_time > remote_time) {
// 远端有,本地有,远端>本地,下载
await
this.download(local_path, remote_path, index_path, remote_time);
} else if (local_time < remote_time) {
// 远端有,本地有,远端<本地,上传
await
this.upload(local_path, remote_path, index_path);
}
} else {
// 远端有,本地无
if (localStorage.getItem(index_path)) {
// 远端有,本地无,记录有,删除远端
await
this.client.deleteFile(remote_path);
} else {
// 远端有,本地无,记录无,下载
await
this.download(local_path, remote_path, index_path, remote_time);
}
}
}
for (const local_path of this.local_files()) {
const remote_path = path.join(root, local_path);
const index_path = '_FILE_' + remote_path;
if (!remote_files.has(local_path)) {
// 远端无,本地有
if (localStorage.getItem(index_path)) {
// 远端无,本地有,记录有,删除本地
window.ygopro.unlink(local_path);
} else {
// 远端无,本地有,记录无,上传
await
this.upload(local_path, remote_path, index_path);
}
}
}
console.log('sync', 'done');
this.working = false;
}
async download(local_path: string, remote_path: string, index_path: string, time: number) {
console.log('download', local_path, remote_path, index_path, time);
const data: Uint8Array = await this.client.getFileContents(remote_path);
window.ygopro.writeFile(local_path, Buffer.from(data.buffer).toString('base64'));
window.ygopro.setFileLastModified(local_path, time);
localStorage.setItem(local_path, time.toString());
}
async upload(local_path: string, remote_path: string, index_path: string) {
console.log('upload', local_path, remote_path, index_path);
const data = Buffer.from(window.ygopro.readFile(local_path), 'base64');
await this.client.putFileContents(remote_path, data);
const item: FileStats = await this.client.stat(remote_path);
const time = Date.parse(item.lastmod);
window.ygopro.setFileLastModified(local_path, time);
localStorage.setItem(local_path, time.toString());
}
local_files() {
return [
...this.local_files_do('deck', '.ydk'),
...this.local_files_do('replay', '.yrp'),
...this.local_files_do('single', '.lua'),
];
}
local_files_do(directory, extname): string[] {
return JSON.parse(window.ygopro.readdir(directory))
.filter(file => path.extname(file) === extname)
.map(file => path.join(directory, file));
}
async *walk(dir: string): AsyncIterable<Stats> {
const items: Stats[] = await this.client.getDirectoryContents(dir);
for (let item of items) {
if (item.type === 'directory') {
yield* this.walk(item.filename);
} else {
yield item;
}
}
}
}
// const local = window.ygopro.getFileStats(item.filename);
// const remote_mtime = Date.parse(item.lastmod);
// if (local.mtime >= remote_mtime) {
// // 本地的较新,上传0
// await client.putFileContents();
//
// }
...@@ -6,6 +6,7 @@ import { sortBy } from 'lodash'; ...@@ -6,6 +6,7 @@ import { sortBy } from 'lodash';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { LoginService } from './login.service'; import { LoginService } from './login.service';
import { ResultDialog } from './result/result.dialog'; import { ResultDialog } from './result/result.dialog';
import { StorageService } from './storage.service';
export interface User { export interface User {
admin: boolean; admin: boolean;
...@@ -144,11 +145,12 @@ export class YGOProService { ...@@ -144,11 +145,12 @@ export class YGOProService {
replay: true replay: true
}]; }];
constructor(private login: LoginService, private http: Http, private dialog: MdDialog) { constructor(private login: LoginService, private http: Http, private dialog: MdDialog, private storage: StorageService) {
this.load().catch(alert); this.load().catch(alert);
} }
async load() { async load() {
const apps: App[] = await this.http.get('https://api.mycard.moe/apps.json').map(response => response.json()).toPromise(); 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')!; const app = apps.find(app => app.id === 'ygopro')!;
this.news = app.news['zh-CN']; this.news = app.news['zh-CN'];
...@@ -163,6 +165,7 @@ export class YGOProService { ...@@ -163,6 +165,7 @@ export class YGOProService {
image_url: topic.image_url && new URL(topic.image_url, 'https://ygobbs.com').toString() image_url: topic.image_url && new URL(topic.image_url, 'https://ygobbs.com').toString()
}))); })));
this.storage.sync('ygopro');
this.load_points(); this.load_points();
await this.load_result(false); await this.load_result(false);
...@@ -214,11 +217,13 @@ export class YGOProService { ...@@ -214,11 +217,13 @@ export class YGOProService {
Observable.fromEvent(document, evtname).subscribe(() => { Observable.fromEvent(document, evtname).subscribe(() => {
if (!document[hidden]) { if (!document[hidden]) {
this.load_result(); this.load_result();
this.storage.sync('ygopro');
} }
}); });
} else { } else {
Observable.fromEvent(window, 'focus').subscribe(() => { Observable.fromEvent(window, 'focus').subscribe(() => {
this.load_result(); this.load_result();
this.storage.sync('ygopro');
}); });
} }
} }
...@@ -415,9 +420,9 @@ export class RoomListDataSource extends DataSource<any> { ...@@ -415,9 +420,9 @@ export class RoomListDataSource extends DataSource<any> {
return rooms.filter(room => room.id != message.data); return rooms.filter(room => room.id != message.data);
} }
}, []); }, []);
// 把多个服务器的数据拼接起来,这里是 combineLatest 的第二个参数 // 把多个服务器的数据拼接起来,这里是 combineLatest 的第二个参数
}), (...sources: Room[][]) => (<Room[]>[]).concat(...sources)) }), (...sources: Room[][]) => (<Room[]>[]).concat(...sources))
// 房间排序 // 房间排序
.map(rooms => sortBy(rooms, (room) => { .map(rooms => sortBy(rooms, (room) => {
if (room.arena === 'athletic') { if (room.arena === 'athletic') {
return 0; return 0;
...@@ -429,7 +434,7 @@ export class RoomListDataSource extends DataSource<any> { ...@@ -429,7 +434,7 @@ export class RoomListDataSource extends DataSource<any> {
return room.options.mode + 2; return room.options.mode + 2;
} }
}) })
// loading、empty、error // loading、empty、error
).filter((rooms) => { ).filter((rooms) => {
this.loading = false; this.loading = false;
this.empty = rooms.length == 0; this.empty = rooms.length == 0;
...@@ -466,6 +471,14 @@ declare global { ...@@ -466,6 +471,14 @@ declare global {
openDrawer(): void openDrawer(): void
backHome(): void backHome(): void
share(text: string): void share(text: string): void
readFile(path: string): string
writeFile(path: string, data: string): string
readdir(path: string): string
unlink(path: string): boolean
getFileLastModified(path: string): number
setFileLastModified(path: string, time: number): void
}; };
} }
} }
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