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

sync

parent a06e52d7
......@@ -5,6 +5,7 @@
"scripts": {
"ng": "ng",
"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",
"postinstall": "./fuck.sh",
"build:dev": "ng build --base-href /mobile2/index.html --locale zh-CN --aot",
......@@ -12,7 +13,6 @@
"publish": "npm run build && ./ossutil cp -rf dist oss://mycard/mobile",
"publish:dev": "npm run build:dev && ./ossutil cp -rf dist oss://mycard/mobile2"
},
"private": true,
"dependencies": {
"@angular/animations": "^4.3.0",
"@angular/cdk": "^2.0.0-beta.8",
......
......@@ -12,7 +12,8 @@ import {
MdIconModule,
MdInputModule,
MdListModule,
MdMenuModule, MdProgressSpinnerModule,
MdMenuModule,
MdProgressSpinnerModule,
MdSelectModule,
MdSlideToggleModule,
MdSnackBarModule,
......@@ -31,13 +32,11 @@ import { MatchDialog } from './match/match.component';
import { NewRoomComponent } from './new-room/new-room.component';
import { ResultDialog } from './result/result.dialog';
import { RoomListComponent } from './room-list/room-list.component';
import { StorageService } from './storage.service';
import { ToolbarComponent } from './toolbar/toolbar.component';
import { WatchComponent } from './watch/watch.component';
import { WindbotComponent } from './windbot/windbot.component';
import { YGOProService } from './ygopro.service';
import { DecksComponent } from './decks/decks.component';
import { DragulaModule } from 'ng2-dragula';
import { SaveService } from './save.service';
@NgModule({
declarations: [
......@@ -50,7 +49,6 @@ import { SaveService } from './save.service';
WatchComponent,
ToolbarComponent,
ResultDialog,
DecksComponent,
],
imports: [
BrowserModule,
......@@ -77,9 +75,8 @@ import { SaveService } from './save.service';
JsonpModule,
MdMenuModule,
MdProgressSpinnerModule,
DragulaModule
],
providers: [YGOProService, SaveService],
providers: [YGOProService, StorageService],
bootstrap: [AppComponent],
entryComponents: [MatchDialog, ResultDialog],
})
......
......@@ -13,6 +13,9 @@
</md-option>
</md-autocomplete>
</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>
<img id="avatar" [src]="login.user.avatar_url">
</a>
......
......@@ -8,7 +8,7 @@ import { LoginService } from '../login.service';
import { MatchDialog } from '../match/match.component';
import { routerTransition2 } from '../router.animations';
import { YGOProService } from '../ygopro.service';
import { SaveService } from '../save.service';
import { StorageService } from '../storage.service';
@Component({
selector: 'app-lobby',
templateUrl: 'lobby.component.html',
......@@ -30,7 +30,7 @@ export class LobbyComponent {
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');
arena_url.searchParams.set('sso', login.token);
......
import { Observable } from 'rxjs/Observable';
// import { bindCallback as staticBindCallback } from '../../observable/bindCallback';
declare module 'rxjs/Observable' {
interface Observable<T> {
[Symbol.asyncIterator](): AsyncIterator<T>;
}
}
class Deferred<T> extends Promise<T> {
resolve;
reject;
constructor() {
let a, b;
super((resolve, reject) => {
a = resolve;
b = reject;
});
this.resolve = a;
this.reject = b;
}
}
Observable.prototype[Symbol.asyncIterator] = async function*() {
let deferred = new Deferred();
const completed = Symbol('completed');
this.subscribe({
next(value){
deferred.resolve(value);
},
error(error){
deferred.reject(error);
},
complete() {
deferred.resolve(completed);
},
});
let value;
while ((value = await deferred) != completed) {
deferred = new Deferred();
yield value;
}
};
// import { Observable } from 'rxjs/Observable';
//
// declare module 'rxjs/Observable' {
// interface Observable<T> {
// [Symbol.asyncIterator](): AsyncIterator<T>;
// }
// }
//
// class Deferred<T> extends Promise<T> {
// resolve;
// reject;
//
// constructor() {
// let a, b;
// super((resolve, reject) => {
// a = resolve;
// b = reject;
// });
// this.resolve = a;
// this.reject = b;
// }
// }
//
// Observable.prototype[Symbol.asyncIterator] = async function*() {
//
// let deferred = new Deferred();
// const completed = Symbol('completed');
//
// this.subscribe({
// next(value){
// deferred.resolve(value);
// },
// error(error){
// deferred.reject(error);
// },
// complete() {
// deferred.resolve(completed);
// },
// });
//
// let value;
// while ((value = await deferred) != completed) {
// deferred = new Deferred();
// 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';
import { Observable } from 'rxjs/Observable';
import { LoginService } from './login.service';
import { ResultDialog } from './result/result.dialog';
import { StorageService } from './storage.service';
export interface User {
admin: boolean;
......@@ -144,11 +145,12 @@ export class YGOProService {
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);
}
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'];
......@@ -163,6 +165,7 @@ export class YGOProService {
image_url: topic.image_url && new URL(topic.image_url, 'https://ygobbs.com').toString()
})));
this.storage.sync('ygopro');
this.load_points();
await this.load_result(false);
......@@ -214,11 +217,13 @@ export class YGOProService {
Observable.fromEvent(document, evtname).subscribe(() => {
if (!document[hidden]) {
this.load_result();
this.storage.sync('ygopro');
}
});
} else {
Observable.fromEvent(window, 'focus').subscribe(() => {
this.load_result();
this.storage.sync('ygopro');
});
}
}
......@@ -415,9 +420,9 @@ export class RoomListDataSource extends DataSource<any> {
return rooms.filter(room => room.id != message.data);
}
}, []);
// 把多个服务器的数据拼接起来,这里是 combineLatest 的第二个参数
// 把多个服务器的数据拼接起来,这里是 combineLatest 的第二个参数
}), (...sources: Room[][]) => (<Room[]>[]).concat(...sources))
// 房间排序
// 房间排序
.map(rooms => sortBy(rooms, (room) => {
if (room.arena === 'athletic') {
return 0;
......@@ -429,7 +434,7 @@ export class RoomListDataSource extends DataSource<any> {
return room.options.mode + 2;
}
})
// loading、empty、error
// loading、empty、error
).filter((rooms) => {
this.loading = false;
this.empty = rooms.length == 0;
......@@ -466,6 +471,14 @@ declare global {
openDrawer(): void
backHome(): 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