Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
M
mycard
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
syntax_j
mycard
Commits
92c66884
Commit
92c66884
authored
Dec 13, 2016
by
神楽坂玲奈
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'v3' of github.com:mycard/mycard into v3
parents
fb3e4ac2
70650f20
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
128 additions
and
57 deletions
+128
-57
app/app-detail.component.html
app/app-detail.component.html
+7
-7
app/app.ts
app/app.ts
+1
-1
app/apps.service.ts
app/apps.service.ts
+104
-35
app/lobby.component.ts
app/lobby.component.ts
+4
-3
app/ygopro.component.html
app/ygopro.component.html
+10
-10
app/ygopro.component.ts
app/ygopro.component.ts
+2
-1
No files found.
app/app-detail.component.html
View file @
92c66884
...
...
@@ -20,13 +20,13 @@
<!--应用ready-->
<div
class=
"actions"
*ngIf=
"currentApp.isReady() && (currentApp.id != 'ygopro')"
>
<button
i18n
*ngIf=
"currentApp.run
able()"
(click)=
"runApp
(currentApp)"
type=
"button"
class=
"btn btn-primary"
>
运行
</button>
<button
i18n
*ngIf=
"currentApp.run
able() && currentApp.actions.get('custom'
)"
(click)=
"custom(currentApp)"
type=
"button"
class=
"btn btn-secondary"
>
设置
</button>
<button
i18n
*ngIf=
"currentApp.run
nable()"
(click)=
"runApp(currentApp)"
[disabled]=
"!appsService.allReady
(currentApp)"
type=
"button"
class=
"btn btn-primary"
>
运行
</button>
<button
i18n
*ngIf=
"currentApp.run
nable() && currentApp.actions.get('custom')"
[disabled]=
"!appsService.allReady(currentApp
)"
(click)=
"custom(currentApp)"
type=
"button"
class=
"btn btn-secondary"
>
设置
</button>
<div
id=
"network"
*ngIf=
"currentApp.network && currentApp.network.protocol == 'maotama'"
>
<div
class=
"input-group"
>
<input
*ngIf=
"appsService.connections.get(currentApp)"
[value]=
"appsService.connections.get(currentApp).address || 'Loading...'"
readonly
type=
"text"
class=
"form-control"
title=
"address"
>
<div
class=
"input-group-btn"
>
<button
i18n
*ngIf=
"!appsService.connections.get(currentApp)"
(click)=
"appsService.network(currentApp, currentApp.network.servers[0])"
type=
"button"
class=
"btn btn-secondary"
>
联机
</button>
<button
i18n
*ngIf=
"!appsService.connections.get(currentApp)"
[disabled]=
"!appsService.allReady(currentApp)"
(click)=
"appsService.network(currentApp, currentApp.network.servers[0])"
type=
"button"
class=
"btn btn-secondary"
>
联机
</button>
<button
i18n
*ngIf=
"appsService.connections.get(currentApp)"
(click)=
"copy(appsService.connections.get(currentApp).address)"
[disabled]=
"!appsService.connections.get(currentApp).address"
type=
"button"
class=
"btn btn-secondary"
>
复制
</button>
<button
type=
"button"
class=
"btn btn-secondary dropdown-toggle dropdown-toggle-split"
data-toggle=
"dropdown"
style=
"height: 38px;"
></button>
<div
class=
"dropdown-menu"
[class.dropdown-menu-right]=
"appsService.connections.get(currentApp)"
>
...
...
@@ -61,10 +61,10 @@
<th
scope=
"row"
>
{{i + 1}}
</th>
<td>
{{mod.name}}
</td>
<td
*ngIf=
"mod.isReady()"
>
<button
i18n
type=
"button"
(click)=
"uninstall(mod)"
class=
"btn btn-danger btn-sm"
>
卸载
</button>
<button
i18n
type=
"button"
[disabled]=
"mod.isInstalled()&&!appsService.allReady(mod)"
(click)=
"uninstall(mod)"
class=
"btn btn-danger btn-sm"
>
卸载
</button>
</td>
<td
*ngIf=
"!mod.isInstalled()"
>
<button
i18n
(click)=
"installMod(mod)"
type=
"button"
*ngIf=
"!mod.isInstalled()"
class=
"btn btn-primary btn-sm"
>
安装
</button>
<button
i18n
(click)=
"installMod(mod)"
[disabled]=
"mod.isInstalled()&&!appsService.allReady(mod)"
type=
"button"
*ngIf=
"!mod.isInstalled()"
class=
"btn btn-primary btn-sm"
>
安装
</button>
</td>
<td
*ngIf=
"mod.isInstalled()&&!mod.isReady()"
>
<progress
class=
"progress progress-striped progress-animated"
value=
"{{mod.status.progress}}"
max=
"{{mod.status.total}}"
></progress>
...
...
@@ -75,9 +75,9 @@
</table>
</div>
<h2
i18n
>
本地文件
</h2>
<button
i18n
(click)=
"appsService.browse(currentApp)"
type=
"button"
class=
"btn btn-secondary"
>
浏览本地文件
</button>
<button
i18n
(click)=
"appsService.browse(currentApp)"
[disabled]=
"!appsService.allReady(currentApp)"
type=
"button"
class=
"btn btn-secondary"
>
浏览本地文件
</button>
<!--<button i18n type="button" (click)="verifyFiles(currentApp)" class="btn btn-secondary">校验完整性</button>-->
<button
i18n
(click)=
"uninstall(currentApp)"
type=
"button"
class=
"btn btn-secondary"
>
卸载
</button>
<button
i18n
(click)=
"uninstall(currentApp)"
[disabled]=
"!appsService.allReady(currentApp)"
type=
"button"
class=
"btn btn-secondary"
>
卸载
</button>
</div>
<!--安装modal-->
...
...
app/app.ts
View file @
92c66884
...
...
@@ -115,7 +115,7 @@ export class App {
return
this
.
status
.
status
===
"
uninstalling
"
;
}
runable
():
boolean
{
run
n
able
():
boolean
{
return
[
Category
.
game
].
includes
(
this
.
category
);
}
...
...
app/apps.service.ts
View file @
92c66884
import
{
Injectable
,
ApplicationRef
,
EventEmitter
}
from
"
@angular/core
"
;
import
{
Injectable
,
ApplicationRef
,
EventEmitter
,
NgZone
}
from
"
@angular/core
"
;
import
{
Http
}
from
"
@angular/http
"
;
import
*
as
crypto
from
"
crypto
"
;
import
{
App
,
AppStatus
,
Action
}
from
"
./app
"
;
...
...
@@ -52,11 +52,26 @@ export class AppsService {
readonly
tarPath
=
process
.
platform
===
"
win32
"
?
path
.
join
(
process
.
env
[
'
NODE_ENV
'
]
==
'
production
'
?
process
.
resourcesPath
:
''
,
'
bin
'
,
'
bsdtar.exe
'
)
:
'
bsdtar
'
;
constructor
(
private
http
:
Http
,
private
settingsService
:
SettingsService
,
private
ref
:
ApplicationRef
,
private
downloadService
:
DownloadService
)
{
private
downloadService
:
DownloadService
,
private
ngZone
:
NgZone
)
{
}
get
lastVisted
():
App
|
undefined
{
let
id
=
localStorage
.
getItem
(
"
last_visited
"
);
if
(
id
)
{
return
this
.
apps
.
get
(
id
);
}
return
undefined
;
}
set
lastVisted
(
app
:
App
|
undefined
)
{
if
(
app
)
{
localStorage
.
setItem
(
"
last_visited
"
,
app
.
id
);
}
}
async
loadApps
()
{
let
data
=
await
this
.
http
.
get
(
'
./apps.json
'
).
map
((
response
)
=>
response
.
json
()).
toPromise
();
let
data
=
await
this
.
http
.
get
(
'
./apps.json
'
).
map
((
response
)
=>
response
.
json
()).
toPromise
();
this
.
apps
=
this
.
loadAppsList
(
data
);
return
this
.
apps
;
}
...
...
@@ -247,6 +262,21 @@ export class AppsService {
return
apps
;
};
allReady
(
app
:
App
)
{
return
app
.
isReady
()
&&
app
.
findDependencies
().
every
((
dependency
)
=>
dependency
.
isReady
())
&&
this
.
findChildren
(
app
).
every
((
child
)
=>
(
child
.
isInstalled
()
&&
child
.
isReady
())
||
!
child
.
isInstalled
());
}
async
importApp
(
app
:
App
,
appPath
:
string
)
{
if
(
!
app
.
isInstalled
())
{
app
.
status
.
status
=
"
ready
"
;
app
.
local
=
new
AppLocal
();
app
.
local
.
path
=
appPath
;
await
this
.
update
(
app
,
true
);
}
}
sha256sum
(
file
:
string
):
Promise
<
string
>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
let
input
=
fs
.
createReadStream
(
file
);
...
...
@@ -258,9 +288,9 @@ export class AppsService {
reject
(
error
);
});
hash
.
on
(
'
readable
'
,
()
=>
{
let
data
=
hash
.
read
();
let
data
=
<
Buffer
>
hash
.
read
();
if
(
data
)
{
resolve
(
(
<
Buffer
>
data
)
.
toString
(
"
hex
"
));
resolve
(
data
.
toString
(
"
hex
"
));
}
});
input
.
pipe
(
hash
);
...
...
@@ -302,7 +332,7 @@ export class AppsService {
}
else
{
readyToUpdate
=
app
.
isReady
()
&&
mods
.
every
((
mod
)
=>
mod
.
isReady
());
}
if
(
readyToUpdate
&&
(
app
.
local
!
.
version
!==
app
.
version
||
verify
))
{
if
(
readyToUpdate
&&
(
verify
||
app
.
local
!
.
version
!==
app
.
version
))
{
app
.
status
.
status
=
"
updating
"
;
try
{
Logger
.
info
(
"
Checking updating:
"
,
app
);
...
...
@@ -333,6 +363,12 @@ export class AppsService {
deletedFiles
.
add
(
file
);
}
}
// changedFiles包含addedFiles,addedFiles仅供mod更新的时候使用。
for
(
let
addedFile
of
addedFiles
)
{
changedFiles
.
add
(
addedFile
);
}
let
backupFiles
:
string
[]
=
[];
let
restoreFiles
:
string
[]
=
[];
if
(
app
.
parent
)
{
...
...
@@ -743,12 +779,19 @@ export class AppsService {
let
option
=
task
.
option
;
let
installDir
=
option
.
installDir
;
let
checksumFile
=
await
this
.
getChecksumFile
(
app
);
let
allFiles
=
new
Set
(
checksumFile
.
keys
());
app
.
status
.
status
=
"
installing
"
;
app
.
status
.
total
=
allFiles
.
size
;
app
.
status
.
progress
=
0
;
let
interval
=
setInterval
(()
=>
{
},
500
);
if
(
app
.
parent
)
{
// mod需要安装到parent路径
installDir
=
app
.
parent
.
local
!
.
path
;
let
parentFiles
=
new
ComparableSet
(
Array
.
from
(
app
.
parent
.
local
!
.
files
.
keys
()));
let
appFiles
=
new
ComparableSet
(
Array
.
from
(
checksumFile
.
keys
()));
let
conflictFiles
=
appFiles
.
intersection
(
parentFiles
);
app
.
status
.
total
+=
conflictFiles
.
size
;
if
(
conflictFiles
.
size
>
0
)
{
let
backupPath
=
path
.
join
(
option
.
installLibrary
,
"
backup
"
,
app
.
parent
.
id
);
// 文件夹不需要备份,删除
...
...
@@ -757,18 +800,23 @@ export class AppsService {
conflictFiles
.
delete
(
conflictFile
);
}
}
await
this
.
backupFiles
(
app
.
parent
.
local
!
.
path
,
backupPath
,
conflictFiles
);
await
new
Promise
((
resolve
,
reject
)
=>
{
this
.
ngZone
.
runOutsideAngular
(
async
()
=>
{
try
{
await
this
.
backupFiles
(
app
.
parent
!
.
local
!
.
path
,
backupPath
,
conflictFiles
,
(
n
)
=>
{
app
.
status
.
progress
+=
1
;
});
resolve
();
}
catch
(
e
)
{
reject
(
e
);
}
});
});
}
}
let
allFiles
=
new
Set
(
checksumFile
.
keys
());
app
.
status
.
status
=
"
installing
"
;
app
.
status
.
total
=
allFiles
.
size
;
app
.
status
.
progress
=
0
;
// let timeNow = new Date().getTime();
for
(
let
file
of
option
.
downloadFiles
)
{
await
this
.
createDirectory
(
installDir
);
let
interval
=
setInterval
(()
=>
{
},
500
);
await
new
Promise
((
resolve
,
reject
)
=>
{
this
.
extract
(
file
,
installDir
).
subscribe
(
(
lastItem
:
string
)
=>
{
...
...
@@ -782,8 +830,8 @@ export class AppsService {
resolve
();
});
});
clearInterval
(
interval
);
}
clearInterval
(
interval
);
await
this
.
postInstall
(
app
,
installDir
);
console
.
log
(
"
post install success
"
);
let
local
=
new
AppLocal
();
...
...
@@ -903,7 +951,8 @@ export class AppsService {
}
}
async
backupFiles
(
dir
:
string
,
backupDir
:
string
,
files
:
Iterable
<
string
>
)
{
async
backupFiles
(
dir
:
string
,
backupDir
:
string
,
files
:
Iterable
<
string
>
,
callback
?:
(
progress
:
number
)
=>
void
)
{
let
n
=
0
;
for
(
let
file
of
files
)
{
await
new
Promise
(
async
(
resolve
,
reject
)
=>
{
let
srcPath
=
path
.
join
(
dir
,
file
);
...
...
@@ -912,19 +961,28 @@ export class AppsService {
fs
.
unlink
(
backupPath
,
(
err
)
=>
{
fs
.
rename
(
srcPath
,
backupPath
,
resolve
);
});
if
(
callback
)
{
callback
(
n
)
}
n
+=
1
;
});
}
}
async
restoreFiles
(
dir
:
string
,
backupDir
:
string
,
files
:
Iterable
<
string
>
)
{
async
restoreFiles
(
dir
:
string
,
backupDir
:
string
,
files
:
Iterable
<
string
>
,
callback
?:
(
progress
:
number
)
=>
{})
{
let
n
=
0
;
for
(
let
file
of
files
)
{
await
new
Promise
((
resolve
,
reject
)
=>
{
let
backupPath
=
path
.
join
(
backupDir
,
file
);
let
srcPath
=
path
.
join
(
dir
,
file
);
fs
.
unlink
(
srcPath
,
(
err
)
=>
{
fs
.
rename
(
backupPath
,
srcPath
,
resolve
);
})
})
});
n
+=
1
;
if
(
callback
)
{
callback
(
n
);
}
});
}
}
...
...
@@ -993,18 +1051,21 @@ export class AppsService {
console
.
error
(
'
doUninstall
'
,
"
无法卸载,还有依赖此程序的游戏。
"
,
app
);
throw
"
无法卸载,还有依赖此程序的游戏。
"
}
app
.
status
.
status
=
"
uninstalling
"
;
let
appDir
=
app
.
local
!
.
path
;
let
files
=
Array
.
from
(
app
.
local
!
.
files
.
keys
()).
sort
().
reverse
();
app
.
status
.
total
=
files
.
length
;
// 500毫秒手动刷新,避免文件过多产生的性能问题
let
interval
=
setInterval
(()
=>
{
},
500
);
await
new
Promise
((
resolve
,
reject
)
=>
{
this
.
ngZone
.
runOutsideAngular
(
async
()
=>
{
try
{
for
(
let
file
of
files
)
{
app
.
status
.
progress
+=
1
;
await
this
.
deleteFile
(
path
.
join
(
appDir
,
file
));
}
if
(
app
.
parent
)
{
// TODO: 建立Library模型,把拼路径的事情交给Library
let
backupDir
=
path
.
join
(
path
.
dirname
(
appDir
),
"
backup
"
,
app
.
parent
.
id
);
...
...
@@ -1015,7 +1076,15 @@ export class AppsService {
await
this
.
restoreFiles
(
appDir
,
backupDir
,
Array
.
from
(
difference
))
}
}
resolve
();
}
catch
(
e
)
{
reject
(
e
);
}
});
});
clearInterval
(
interval
);
app
.
reset
()
}
}
\ No newline at end of file
app/lobby.component.ts
View file @
92c66884
...
...
@@ -31,10 +31,10 @@ export class LobbyComponent implements OnInit {
async
ngOnInit
()
{
this
.
apps
=
await
this
.
appsService
.
loadApps
();
await
this
.
appsService
.
migrate
();
for
(
let
app
of
this
.
apps
.
values
())
{
for
(
let
app
of
this
.
apps
.
values
())
{
this
.
appsService
.
update
(
app
);
}
this
.
chooseApp
(
Array
.
from
(
this
.
apps
.
values
()).
find
(
app
=>
app
.
isInstalled
())
||
this
.
apps
.
get
(
"
ygopro
"
)
!
);
this
.
chooseApp
(
this
.
appsService
.
lastVisted
||
this
.
apps
.
get
(
"
ygopro
"
)
!
);
// 初始化聊天室
let
url
=
new
URL
(
'
candy/index.html
'
,
location
.
href
);
...
...
@@ -42,7 +42,7 @@ export class LobbyComponent implements OnInit {
params
.
set
(
'
jid
'
,
this
.
loginService
.
user
.
username
+
'
@mycard.moe
'
);
params
.
set
(
'
password
'
,
this
.
loginService
.
user
.
external_id
.
toString
());
params
.
set
(
'
nickname
'
,
this
.
loginService
.
user
.
username
);
switch
(
this
.
settingsService
.
getLocale
())
{
switch
(
this
.
settingsService
.
getLocale
())
{
case
'
zh-CN
'
:
params
.
set
(
'
language
'
,
'
cn
'
);
break
;
...
...
@@ -57,6 +57,7 @@ export class LobbyComponent implements OnInit {
chooseApp
(
app
:
App
)
{
this
.
currentApp
=
app
;
this
.
appsService
.
lastVisted
=
app
;
if
(
this
.
candy
&&
this
.
currentApp
.
conference
)
{
(
<
WebViewElement
>
this
.
candy
.
nativeElement
).
send
(
'
join
'
,
this
.
currentApp
.
conference
+
'
@conference.mycard.moe
'
);
}
...
...
app/ygopro.component.html
View file @
92c66884
...
...
@@ -5,19 +5,19 @@
<option
*ngFor=
"let deck of decks"
[ngValue]=
"deck"
>
{{deck}}
</option>
</select>
</div>
<button
i18n
type=
"submit"
class=
"btn btn-secondary"
(click)=
"edit_deck(current_deck)"
>
编辑
</button>
<button
i18n
type=
"submit"
(click)=
"delete_deck(current_deck)"
class=
"btn btn-secondary"
>
删除
</button>
<button
i18n
types
=
"submit"
(click)=
"refresh()"
class=
"btn btn-secondary"
>
刷新
</button>
<button
i18n
[disabled]=
"!appsService.allReady(app)"
type=
"submit"
class=
"btn btn-secondary"
(click)=
"edit_deck(current_deck)"
>
编辑
</button>
<button
i18n
[disabled]=
"!appsService.allReady(app)"
type=
"submit"
(click)=
"delete_deck(current_deck)"
class=
"btn btn-secondary"
>
删除
</button>
<button
i18n
[disabled]=
"!appsService.allReady(app)"
type
=
"submit"
(click)=
"refresh()"
class=
"btn btn-secondary"
>
刷新
</button>
</form>
<div
class=
"actions"
>
<button
i18n
(click)=
"request_match('athletic')"
*ngIf=
"matching_arena != 'athletic'"
[disabled]=
"matching
"
type=
"button"
class=
"btn btn-primary"
>
竞技匹配
</button>
<button
i18n
(click)=
"cancel_match()"
*ngIf=
"matching_arena == 'athletic'"
type=
"button"
class=
"btn btn-primary"
>
取消等待
</button>
<button
i18n
(click)=
"request_match('entertain')"
*ngIf=
"matching_arena != 'entertain'"
[disabled]=
"matching
"
type=
"button"
class=
"btn btn-secondary"
>
娱乐匹配
</button>
<button
i18n
(click)=
"cancel_match()"
*ngIf=
"matching_arena == 'entertain'"
type=
"button"
class=
"btn btn-secondary"
>
取消等待
</button>
<button
i18n
[disabled]=
"matching"
type=
"button"
class=
"btn btn-secondary"
data-toggle=
"modal"
data-target=
"#game-create-modal"
>
创建房间
</button>
<button
i18n
[disabled]=
"matching"
type=
"button"
class=
"btn btn-secondary"
data-toggle=
"modal"
data-target=
"#game-list-modal"
>
房间列表
</button>
<button
i18n
type=
"button"
class=
"btn btn-secondary"
data-toggle=
"modal"
data-target=
"#game-create-windbot"
>
单人模式
</button>
<button
i18n
[disabled]=
"matching||!appsService.allReady(app)"
(click)=
"request_match('athletic')"
*ngIf=
"matching_arena != 'athletic'
"
type=
"button"
class=
"btn btn-primary"
>
竞技匹配
</button>
<button
i18n
[disabled]=
"!appsService.allReady(app)"
(click)=
"cancel_match()"
*ngIf=
"matching_arena == 'athletic'"
type=
"button"
class=
"btn btn-primary"
>
取消等待
</button>
<button
i18n
[disabled]=
"matching||!appsService.allReady(app)"
(click)=
"request_match('entertain')"
*ngIf=
"matching_arena != 'entertain'
"
type=
"button"
class=
"btn btn-secondary"
>
娱乐匹配
</button>
<button
i18n
[disabled]=
"!appsService.allReady(app)"
(click)=
"cancel_match()"
*ngIf=
"matching_arena == 'entertain'"
type=
"button"
class=
"btn btn-secondary"
>
取消等待
</button>
<button
i18n
[disabled]=
"matching
||!appsService.allReady(app)
"
type=
"button"
class=
"btn btn-secondary"
data-toggle=
"modal"
data-target=
"#game-create-modal"
>
创建房间
</button>
<button
i18n
[disabled]=
"matching
||!appsService.allReady(app)
"
type=
"button"
class=
"btn btn-secondary"
data-toggle=
"modal"
data-target=
"#game-list-modal"
>
房间列表
</button>
<button
i18n
[disabled]=
"!appsService.allReady(app)"
type=
"button"
class=
"btn btn-secondary"
data-toggle=
"modal"
data-target=
"#game-create-windbot"
>
单人模式
</button>
</div>
<div
class=
"modal fade"
id=
"game-create-windbot"
tabindex=
"-1"
role=
"dialog"
aria-labelledby=
"myModalLabel"
aria-hidden=
"true"
>
...
...
app/ygopro.component.ts
View file @
92c66884
...
...
@@ -15,6 +15,7 @@ import {Http, Headers, URLSearchParams} from "@angular/http";
import
"
rxjs/Rx
"
;
import
{
ISubscription
}
from
"
rxjs/Subscription
"
;
import
{
SettingsService
}
from
"
./settings.sevices
"
;
import
{
AppsService
}
from
"
./apps.service
"
;
declare
const
$
:
any
;
...
...
@@ -108,7 +109,7 @@ export class YGOProComponent implements OnInit {
connections
:
WebSocket
[]
=
[];
constructor
(
private
http
:
Http
,
private
settingsService
:
Setting
sService
,
private
loginService
:
LoginService
,
private
ref
:
ChangeDetectorRef
)
{
constructor
(
private
http
:
Http
,
private
appsService
:
App
sService
,
private
loginService
:
LoginService
,
private
ref
:
ChangeDetectorRef
)
{
switch
(
process
.
platform
)
{
case
'
darwin
'
:
this
.
numfont
=
[
'
/System/Library/Fonts/SFNSTextCondensed-Bold.otf
'
];
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment