├── public
├── favicon.ico
├── images
│ ├── icon.png
│ ├── toolbar
│ │ └── background.png
│ ├── default-project-icon.png
│ ├── tabs
│ │ ├── close.svg
│ │ └── close-active.svg
│ ├── project
│ │ ├── run.svg
│ │ ├── stop.svg
│ │ ├── build.svg
│ │ ├── go-to-hub.svg
│ │ └── debug.svg
│ ├── actions
│ │ └── filter.svg
│ ├── entries
│ │ └── filter-all.svg
│ └── controls
│ │ ├── notifications-enabled.svg
│ │ ├── notifications-disabled-hover.svg
│ │ ├── notifications-disabled.svg
│ │ └── notifications-enabled-hover.svg
├── locales
│ ├── zh-TW
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── project.json
│ │ └── common.json
│ ├── en
│ │ ├── build.json
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── it
│ │ ├── badges.json
│ │ ├── build.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── ja
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── ru
│ │ ├── build.json
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── sv
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── th
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── ca
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── de
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── es
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ ├── fr
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
│ └── pt-BR
│ │ ├── build.json
│ │ ├── badges.json
│ │ ├── login.json
│ │ ├── hub.json
│ │ ├── common.json
│ │ └── project.json
└── fonts
│ └── Roboto
│ ├── Roboto.woff
│ ├── Roboto-Black.woff
│ ├── Roboto-Bold.woff
│ ├── Roboto-Light.woff
│ ├── Roboto-Thin.woff
│ ├── Roboto-Italic.woff
│ ├── Roboto-Black-Italic.woff
│ ├── Roboto-Bold-Italic.woff
│ ├── Roboto-Light-Italic.woff
│ └── Roboto-Thin-Italic.woff
├── client
├── index.d.ts
├── tsconfig.json
├── serverBuild
│ ├── index.pug
│ └── index.styl
├── login
│ ├── index.styl
│ ├── index.ts
│ └── index.pug
├── build
│ ├── index.pug
│ ├── index.styl
│ └── index.ts
├── shared.styl
├── project
│ ├── sidebar
│ │ └── index.ts
│ ├── tabs
│ │ └── homeTab.ts
│ └── index.pug
├── hub
│ ├── index.pug
│ └── index.styl
└── gulpfile.js
├── server
├── tsconfig.json
├── getLocalizedFilename.ts
├── config.ts
├── gulpfile.js
├── passportMiddleware.ts
├── index.d.ts
├── commands
│ ├── registry.ts
│ ├── uninstall.ts
│ └── install.ts
├── schemas.ts
├── index.ts
├── ProjectHub.ts
├── migrateProject.ts
└── BaseRemoteClient.ts
├── SupCore
├── tsconfig.json
├── Data
│ ├── Badges.ts
│ ├── RoomUsers.ts
│ ├── Resources.ts
│ ├── Projects.ts
│ ├── Base
│ │ ├── Hash.ts
│ │ ├── Asset.ts
│ │ ├── Resource.ts
│ │ └── Dictionary.ts
│ ├── index.ts
│ ├── Rooms.ts
│ ├── Assets.ts
│ ├── ProjectManifest.ts
│ └── Room.ts
├── index.ts
├── gulpfile.js
├── ProjectServer.d.ts
└── systems.ts
├── SupClient
├── tsconfig.json
├── loadScript.ts
├── fetch.ts
├── readFile.ts
├── styles
│ ├── reset.styl
│ ├── toolbar.styl
│ ├── properties.styl
│ ├── dialogs.styl
│ ├── buttons.styl
│ ├── resizeHandle.styl
│ ├── treeView.styl
│ ├── tabStrip.styl
│ └── Roboto.styl
├── gulpfile.js
├── html.ts
└── typings
│ └── SupApp.d.ts
├── CONTRIBUTING.md
├── .gitignore
├── registry.json
├── .vscode
├── settings.json
└── launch.json
├── LICENSE.txt
├── tslint.json
├── .travis.yml
├── scripts
├── getBuildPaths.js
├── i18n.js
└── pluginGulpfile.js
├── README.md
├── CODE_OF_CONDUCT.md
└── package.json
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/images/icon.png
--------------------------------------------------------------------------------
/public/locales/zh-TW/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "缺少必要的套件",
3 | "draft": "草稿"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/en/build.json:
--------------------------------------------------------------------------------
1 | {
2 | "showDetails": "Show details",
3 | "hideDetails": "Hide details"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/en/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Missing Dependencies",
3 | "draft": "Draft"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/it/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Dipendenze Mancanti",
3 | "draft": "Bozza"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/ja/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Missing Dependencies",
3 | "draft": "ドラフト"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/ru/build.json:
--------------------------------------------------------------------------------
1 | {
2 | "showDetails": "Показать детали",
3 | "hideDetails": "Скрыть детали"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/sv/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Beroenden saknas",
3 | "draft": "Utkast"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/th/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "ขาดแหล่งอ้างอิง",
3 | "draft": "ฉบับร่าง"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/ca/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Falten dependències",
3 | "draft": "Esborrany"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/de/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Fehlende Abhängigkeiten",
3 | "draft": "Entwurf"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/es/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Faltan dependencias",
3 | "draft": "Borrador"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/fr/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Dépendances manquantes",
3 | "draft": "Brouillon"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/it/build.json:
--------------------------------------------------------------------------------
1 | {
2 | "showDetails": "Mostra dettagli",
3 | "hideDetails": "Nascondi dettagli"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/ru/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Потерянные Зависимости",
3 | "draft": "Черновик"
4 | }
5 |
--------------------------------------------------------------------------------
/client/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto.woff
--------------------------------------------------------------------------------
/public/locales/pt-BR/build.json:
--------------------------------------------------------------------------------
1 | {
2 | "showDetails": "Mostrar detalhes",
3 | "hideDetails": "Ocultar detalhes"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/pt-BR/badges.json:
--------------------------------------------------------------------------------
1 | {
2 | "missingDependencies": "Dependências Não Encontradas",
3 | "draft": "Rascunho"
4 | }
5 |
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Black.woff
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Light.woff
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Thin.woff
--------------------------------------------------------------------------------
/public/images/toolbar/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/images/toolbar/background.png
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Italic.woff
--------------------------------------------------------------------------------
/public/images/default-project-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/images/default-project-icon.png
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Black-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Black-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Bold-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Bold-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Light-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Light-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Roboto/Roboto-Thin-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superpowers/superpowers-core/HEAD/public/fonts/Roboto/Roboto-Thin-Italic.woff
--------------------------------------------------------------------------------
/public/locales/de/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "Willkommen auf diesem Superpowers Server!",
3 | "password": "Server Passwort",
4 | "username": "Benutzername",
5 | "logIn": "Login"
6 | }
--------------------------------------------------------------------------------
/public/locales/ca/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "Benvingut al servidor de Superpowers!",
3 | "password": "Contraseña del servidor",
4 | "username": "Nom d'usuari",
5 | "logIn": "Entrar"
6 | }
--------------------------------------------------------------------------------
/public/locales/th/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "ยินดีต้อนรับเข้าใช้งาน Superpowers !",
3 | "password": "รหัสผ่านเซิฟเวอร์",
4 | "username": "ชื่อผู้ใช้งาน",
5 | "logIn": "เข้าสู่ระบบ"
6 | }
--------------------------------------------------------------------------------
/public/locales/es/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "¡Bienvenido al servidor de Superpowers!",
3 | "password": "Contraseña del servidor",
4 | "username": "Nombre de usuario",
5 | "logIn": "Entrar"
6 | }
--------------------------------------------------------------------------------
/public/locales/ja/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "Superpowersサーバへようこそ!",
3 | "password": "サーバ―パスワード",
4 | "username": "ユーザー名",
5 | "usernamePatternDescription": "妥当な文字は: 英数字, -, アンダースコア.",
6 | "logIn": "ログイン"
7 | }
--------------------------------------------------------------------------------
/public/locales/zh-TW/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "歡迎來到 Superpowers 主機!",
3 | "password": "主機密碼",
4 | "username": "使用者名稱",
5 | "usernamePatternDescription": "可用的字元為: 文字, 數字, - 或底線.",
6 | "logIn": "登入"
7 | }
8 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "noUnusedLocals": true,
7 | "rootDir": "./"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/SupCore/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "noUnusedLocals": true,
7 | "rootDir": "./",
8 | "typeRoots": [ "../node_modules/@types" ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "noUnusedLocals": true,
7 | "rootDir": "./",
8 | "typeRoots": [ "../node_modules/@types" ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/SupClient/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "noUnusedLocals": true,
7 | "rootDir": "./",
8 | "typeRoots": [ "../node_modules/@types" ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/getLocalizedFilename.ts:
--------------------------------------------------------------------------------
1 | export default function getLocalizedFilename(filename: string, languageCode: string) {
2 | if (languageCode === "en") return filename;
3 | const [ basename, extension ] = filename.split(".");
4 | return `${basename}.${languageCode}.${extension}`;
5 | }
6 |
--------------------------------------------------------------------------------
/public/locales/sv/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "Välkommen till denna Superpowers server!",
3 | "password": "Server lösenord",
4 | "username": "Användarnamn",
5 | "usernamePatternDescription": "Giltiga tecken är: bokstäver, siffror, bindestreck och understreck.",
6 | "logIn": "Logga in"
7 | }
--------------------------------------------------------------------------------
/client/serverBuild/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title Superpowers
5 | meta(charset="utf-8")
6 | link(rel="stylesheet",href="/styles/reset.css")
7 | link(rel="stylesheet",href="/serverBuild/index.css")
8 |
9 | body
10 | header= t("common:states.loading")
11 |
--------------------------------------------------------------------------------
/client/serverBuild/index.styl:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | flex-flow: column;
4 | align-items: center;
5 | justify-content: center;
6 | padding: 2em;
7 | background: #eee;
8 | }
9 |
10 | header {
11 | text-transform: uppercase;
12 | font-size: 1.5em;
13 | color: #888;
14 | }
15 |
--------------------------------------------------------------------------------
/public/locales/en/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Login",
3 | "welcome": "Welcome to this Superpowers server!",
4 | "password": "Server password",
5 | "username": "Username",
6 | "usernamePatternDescription": "Valid characters are: letters, numbers, dashes and underscores.",
7 | "logIn": "Log in"
8 | }
--------------------------------------------------------------------------------
/public/locales/ru/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Вход",
3 | "welcome": "Привествуем на сервере Superpowers!",
4 | "password": "Пароль сервера",
5 | "username": "Имя пользователя",
6 | "usernamePatternDescription": "Допустимые символы: буквы, цифры, тире и подчеркивания.",
7 | "logIn": "Войти"
8 | }
9 |
--------------------------------------------------------------------------------
/public/locales/fr/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "welcome": "Bienvenue sur ce serveur Superpowers !",
3 | "password": "Mot de passe du serveur",
4 | "username": "Nom d'utilisateur",
5 | "usernamePatternDescription": "Les caractères valides sont : lettres, chiffres, tirets et tirets du bas.",
6 | "logIn": "Connexion"
7 | }
8 |
--------------------------------------------------------------------------------
/public/locales/pt-BR/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Login",
3 | "welcome": "Bem-vindo a este servidor de Superpowers!",
4 | "password": "Senha do Servidor",
5 | "username": "Nome de Usuário",
6 | "usernamePatternDescription": "Caracteres válidos: letras, números, traços e sublinhados.",
7 | "logIn": "Entrar"
8 | }
--------------------------------------------------------------------------------
/public/locales/it/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Login",
3 | "welcome": "Benvenuto in questo server di Superpowers!",
4 | "password": "Password del server",
5 | "username": "Nome utente",
6 | "usernamePatternDescription": "I caratteri permessi sono: lettere, numeri, trattini ed underscore.",
7 | "logIn": "Login"
8 | }
9 |
--------------------------------------------------------------------------------
/SupClient/loadScript.ts:
--------------------------------------------------------------------------------
1 | export default function loadScript(url: string, callback: Function) {
2 | const script = document.createElement("script");
3 | script.src = url;
4 | script.addEventListener("load", () => { callback(); } );
5 | script.addEventListener("error", () => { callback(); } );
6 | document.body.appendChild(script);
7 | }
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Superpowers
2 |
3 | Thanks for your interest in contributing, We're humbled and to be honest, a bit excited ^_^.
4 | There are many ways you can help advance Superpowers!
5 |
6 | Please check out the [How to Contribute](http://docs.superpowers-html5.com/en/development/how-to-contribute) page in the documentation.
7 |
--------------------------------------------------------------------------------
/SupCore/Data/Badges.ts:
--------------------------------------------------------------------------------
1 | import ListById from "./Base/ListById";
2 |
3 | export default class Badges extends ListById {
4 | static schema: SupCore.Data.Schema = {
5 | id: { type: "string" },
6 | type: { type: "string" },
7 | data: { type: "any" }
8 | };
9 |
10 | constructor(pub: SupCore.Data.BadgeItem[]) {
11 | super(pub, Badges.schema);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/login/index.styl:
--------------------------------------------------------------------------------
1 | @import "../shared"
2 |
3 | body
4 | background #eee
5 | display flex
6 | flex-flow column
7 |
8 | .login, .connecting
9 | flex 1
10 | display flex
11 | flex-flow column
12 | align-items center
13 | justify-content center
14 |
15 | .login .welcome
16 | margin 1em
17 |
18 | .login table.properties
19 | width auto
20 |
21 | .login .buttons
22 | margin 1em
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | *.sublime-*
3 | npm-debug.log
4 | /config.json
5 | /settings.json
6 | /sessions.json
7 | /authorizationsByOrigin.json
8 |
9 | SupCore/**/*.js
10 | SupClient/**/*.js
11 | server/**/*.js
12 | client/**/*.js
13 | !gulpfile.js
14 |
15 | public/*
16 | !public/locales
17 | !public/images
18 | !public/fonts
19 | !public/favicon.ico
20 |
21 | workbench/
22 | packages/
23 | systems/
24 | projects/
25 | builds/
26 |
--------------------------------------------------------------------------------
/registry.json:
--------------------------------------------------------------------------------
1 | {
2 | "game": {
3 | "repository": "https://github.com/superpowers/superpowers-game",
4 | "plugins": {
5 | "florentpoujol/ftext": "https://github.com/florentpoujol/superpowers-game-ftext-plugin"
6 | }
7 | },
8 | "love2d": {
9 | "repository": "https://github.com/superpowers/superpowers-love2d",
10 | "plugins": {}
11 | },
12 | "web": {
13 | "repository": "https://github.com/superpowers/superpowers-web",
14 | "plugins": {}
15 | }
16 | }
--------------------------------------------------------------------------------
/SupCore/Data/RoomUsers.ts:
--------------------------------------------------------------------------------
1 | import ListById from "./Base/ListById";
2 |
3 | export default class RoomUsers extends ListById {
4 | static schema: SupCore.Data.Schema = {
5 | // TODO: use userId for id when we've got proper login
6 | id: { type: "string", minLength: 3, maxLength: 20 },
7 | connectionCount: { type: "number", min: 1 }
8 | // username: { type: "string", minLength: 3, maxLength: 20 }
9 | };
10 |
11 | constructor(pub: any) {
12 | super(pub, RoomUsers.schema);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "editor.tabSize": 2,
4 | "editor.insertSpaces": true,
5 |
6 | "files.exclude": {
7 | ".vscode": true,
8 | "builds/**": true,
9 | "**/*.js": { "when": "$(basename).ts"},
10 | "**/*.js.map": true
11 | },
12 | "search.exclude": {
13 | "**/node_modules": true,
14 | "projects/**": true,
15 | "workbench": true
16 | }
17 | ,
18 | "typescript.tsdk": "./node_modules/typescript/lib"
19 | }
20 |
--------------------------------------------------------------------------------
/public/locales/zh-TW/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} on port ${port}",
3 | "newProject": {
4 | "title": "新專案",
5 | "prompt": "輸入專案名稱並選擇類別",
6 | "namePlaceholder": "專案名稱",
7 | "descriptionPlaceholder": "描述 (非必填)",
8 | "emptyProject": {
9 | "title": "空白專案",
10 | "description": "建立一個空白專案"
11 | },
12 | "autoOpen": "建立後開啟"
13 | },
14 | "editDetails": {
15 | "title": "編輯詳細訊息",
16 | "prompt": "編輯專案的詳細訊息"
17 | },
18 | "openProject": "開啟專案",
19 | "language": "語言"
20 | }
21 |
--------------------------------------------------------------------------------
/client/build/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title Superpowers
5 | meta(charset="utf-8")
6 | link(rel="stylesheet",href="/styles/reset.css")
7 | link(rel="stylesheet",href="/build/index.css")
8 |
9 | body
10 | header= t("common:states.loading")
11 | progress
12 |
13 | .info
14 | .status
15 | button.toggle-details= t("build:showDetails")
16 |
17 | .details(hidden)
18 | ol
19 |
20 | script(src="/SupCore.js")
21 | script(src="/SupClient.js")
22 | script(src="index.js")
23 |
--------------------------------------------------------------------------------
/public/locales/ja/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} on port ${port}",
3 | "newProject": {
4 | "title": "新規プロジェクト",
5 | "prompt": "名前を入力して、プロジェクトのタイプを選択してください。",
6 | "namePlaceholder": "プロジェクト名",
7 | "descriptionPlaceholder": "詳細 (オプション)",
8 | "emptyProject": {
9 | "title": "空のプロジェクト",
10 | "description": "何もない状態で開始する空のプロジェクトです。"
11 | },
12 | "autoOpen": "作成後すぐに開く"
13 | },
14 | "editDetails": {
15 | "title": "詳細を編集",
16 | "prompt": "プロジェクトの詳細を編集"
17 | },
18 | "openProject": "プロジェクトを開く",
19 | "language": "言語"
20 | }
--------------------------------------------------------------------------------
/SupCore/index.ts:
--------------------------------------------------------------------------------
1 | import * as Data from "./Data";
2 |
3 | export { Data };
4 |
5 | export let systemsPath: string;
6 |
7 | export function setSystemsPath(path: string) {
8 | systemsPath = path;
9 | }
10 |
11 | export * from "./systems";
12 |
13 | export function log(message: string): void {
14 | const date = new Date();
15 | const text = `${date.toLocaleDateString()} ${date.toLocaleTimeString()} - ${message}`;
16 | console.log(text);
17 | return;
18 | }
19 |
20 | export class LocalizedError {
21 | constructor(public key: string, public variables: { [key: string]: string; }) {}
22 | }
23 |
--------------------------------------------------------------------------------
/server/config.ts:
--------------------------------------------------------------------------------
1 | export interface Config {
2 | serverName?: string;
3 | mainPort: number;
4 | buildPort: number;
5 | password: string;
6 | sessionSecret: string;
7 | maxRecentBuilds: number;
8 | [key: string]: any;
9 | }
10 |
11 | export const defaults: Config = {
12 | serverName: null,
13 | mainPort: 4237,
14 | buildPort: 4238,
15 | password: "",
16 | sessionSecret: null,
17 | maxRecentBuilds: 10
18 | };
19 |
20 | // Loaded by start.ts
21 | export let server: Config = null;
22 |
23 | export function setServerConfig(serverConfig: Config) {
24 | server = serverConfig;
25 | }
26 |
--------------------------------------------------------------------------------
/server/gulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const gulp = require("gulp");
4 |
5 | // TypeScript
6 | const ts = require("gulp-typescript");
7 | const tsProject = ts.createProject("./tsconfig.json");
8 | const tslint = require("gulp-tslint");
9 |
10 | gulp.task("typescript", () => {
11 | const tsResult = tsProject.src()
12 | .pipe(tslint({ formatter: "prose" }))
13 | .pipe(tslint.report({ emitError: true }))
14 | .on("error", (err) => { throw err; })
15 | .pipe(tsProject())
16 | return tsResult.js.pipe(gulp.dest("./"));
17 | });
18 |
19 | // All
20 | gulp.task("default", gulp.series("typescript"));
21 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "configurations": [
4 | {
5 | "name": "Server",
6 | "request": "launch",
7 | "type": "node",
8 | "program": "${workspaceRoot}/server/index.js",
9 | "stopOnEntry": false,
10 | "args": ["start"],
11 | "runtimeExecutable": null,
12 | "runtimeArgs": ["--nolazy"],
13 | "env": {},
14 | "sourceMaps": true
15 | },
16 | {
17 | "name": "Attach",
18 | "type": "node",
19 | "request": "attach",
20 | "address": "localhost",
21 | "port": 8000,
22 | "sourceMaps": true
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/public/locales/th/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} บน พอร์ต ${port}",
3 | "newProject": {
4 | "title": "สร้างโปรเจคใหม่",
5 | "prompt": "กรอกชื่อและเลือกรูปแบบของโปรเจค",
6 | "namePlaceholder": "ชื่อโปรเจค",
7 | "descriptionPlaceholder": "คำอธิบายโปรเจค (ตัวเลือก)",
8 | "emptyProject": {
9 | "title": "โปรเจคเปล่า",
10 | "description": "สร้างโปรเจคเปล่าๆขึ้นมา โดยไม่ใช้เทมเพลตใดๆ"
11 | },
12 | "autoOpen": "เปิดโปรเจคหลังจากสร้าง"
13 | },
14 | "editDetails": {
15 | "title": "แก้ไขข้อมูล",
16 | "prompt": "แก้ไขข้อมูลของโปรเจค"
17 | },
18 | "openProject": "เปิดโปรเจค",
19 | "language": "ภาษา"
20 | }
--------------------------------------------------------------------------------
/SupClient/fetch.ts:
--------------------------------------------------------------------------------
1 | export default function fetch(url: string, type: XMLHttpRequestResponseType, callback: (err: Error, data?: any) => void) {
2 | const xhr = new XMLHttpRequest();
3 | xhr.open("GET", url, true);
4 | xhr.responseType = type;
5 |
6 | xhr.onload = (event) => {
7 | if (xhr.status !== 200 && xhr.status !== 0) {
8 | callback(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
9 | return;
10 | }
11 |
12 | callback(null, xhr.response);
13 | };
14 |
15 | xhr.onerror = (event) => {
16 | console.log(event);
17 | callback(new Error(`Network error: ${(event.target as any).status}`));
18 | };
19 |
20 | xhr.send();
21 | }
22 |
--------------------------------------------------------------------------------
/public/locales/sv/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} på port ${port}",
3 | "newProject": {
4 | "title": "Nytt projekt",
5 | "prompt": "Fyll i namn och välj typ för ditt nya projekt.",
6 | "namePlaceholder": "Projektnamn",
7 | "descriptionPlaceholder": "Beskrivning (valfritt)",
8 | "emptyProject": {
9 | "title": "Tomt projekt",
10 | "description": "Ett tomt projekt för att börja från noll."
11 | },
12 | "autoOpen": "Öppna direkt"
13 | },
14 | "editDetails": {
15 | "title": "Redigera detaljer",
16 | "prompt": "Redigera projektets detaljer."
17 | },
18 | "openProject": "Öppna projekt",
19 | "language": "Språk"
20 | }
--------------------------------------------------------------------------------
/public/locales/ca/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} al port ${port}",
3 | "newProject": {
4 | "title": "Nou Projecte",
5 | "prompt": "Escriu un nom i selecciona un tipus de projecte.",
6 | "namePlaceholder": "Nom del projecte",
7 | "descriptionPlaceholder": "Descripció (opcional)",
8 | "emptyProject": {
9 | "title": "Projecte buit",
10 | "description": "Un projecte en blanc per començar desde zero."
11 | },
12 | "autoOpen": "Obrir després de crear"
13 | },
14 | "editDetails": {
15 | "title": "Editar detalls",
16 | "prompt": "Editar els detalls del projecte"
17 | },
18 | "openProject": "Obrir Projecte",
19 | "language": "Idioma"
20 | }
--------------------------------------------------------------------------------
/public/locales/de/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} auf Port ${port}",
3 | "newProject": {
4 | "title": "Neues Projekt",
5 | "prompt": "Trage einen Namen ein und wähle einen Typ für das neue Projekt.",
6 | "namePlaceholder": "Projektname",
7 | "descriptionPlaceholder": "Beschreibung (optional)",
8 | "emptyProject": {
9 | "title": "Leeres Projekt",
10 | "description": "Ein leeres Projekt um von vorne zu beginnen."
11 | },
12 | "autoOpen": "Öffne nach Erstellung"
13 | },
14 | "editDetails": {
15 | "title": "Details bearbeiten",
16 | "prompt": "Bearbeite die Projektdetails."
17 | },
18 | "openProject": "Öffne Projekt",
19 | "language": "Sprache"
20 | }
--------------------------------------------------------------------------------
/public/locales/es/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} en el puerto ${port}",
3 | "newProject": {
4 | "title": "Nuevo proyecto",
5 | "prompt": "Escribe un nombre y selecciona un tipo de proyecto.",
6 | "namePlaceholder": "Nombre del proyecto",
7 | "descriptionPlaceholder": "Descripción (opcional)",
8 | "emptyProject": {
9 | "title": "Proyecto vacío",
10 | "description": "Un proyecto en blanco para empezar desde cero."
11 | },
12 | "autoOpen": "Abrir después de crear"
13 | },
14 | "editDetails": {
15 | "title": "Editar detalles",
16 | "prompt": "Editar los detalles del proyecto"
17 | },
18 | "openProject": "Abrir Proyecto",
19 | "language": "Idioma"
20 | }
--------------------------------------------------------------------------------
/public/locales/ru/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Хаб",
3 | "serverAddress": "${hostname} порт: ${port}",
4 | "newProject": {
5 | "title": "Новый проект",
6 | "prompt": "Введите имя и выберите тип нового проекта.",
7 | "namePlaceholder": "Имя проекта",
8 | "descriptionPlaceholder": "Описание (не обязательно)",
9 | "emptyProject": {
10 | "title": "Пустой проект",
11 | "description": "Пустой проект подходит для создания проекта с нуля."
12 | },
13 | "autoOpen": "Открыть после создания"
14 | },
15 | "editDetails": {
16 | "title": "Изменить детали",
17 | "prompt": "Изменить детали проекта."
18 | },
19 | "openProject": "Открыть проект",
20 | "language": "Язык"
21 | }
22 |
--------------------------------------------------------------------------------
/server/passportMiddleware.ts:
--------------------------------------------------------------------------------
1 | import * as passport from "passport";
2 | import { Strategy as LocalStrategy } from "passport-local";
3 |
4 | // NOTE: The regex must match the pattern and min/max lengths in client/login/index.pug
5 | const usernameRegex = /^[A-Za-z0-9_-]{3,20}$/;
6 |
7 | passport.serializeUser((user, done) => { done(null, user.username); });
8 | passport.deserializeUser((username, done) => { done(null, { username }); });
9 |
10 | const strategy = new LocalStrategy((username, password, done) => {
11 | if (!usernameRegex.test(username)) return done(null, false, { message: "invalidUsername" });
12 | done(null, { username });
13 | });
14 |
15 | passport.use(strategy);
16 |
17 | export default passport;
18 |
--------------------------------------------------------------------------------
/client/shared.styl:
--------------------------------------------------------------------------------
1 | .server-header {
2 | border-bottom: 1px solid rgba(0,0,0,0.2);
3 | background: #fff;
4 | display: flex;
5 | align-items: center;
6 |
7 | .server-icon {
8 | width: 40px;
9 | height: 40px;
10 | border: 1px solid rgba(0,0,0,0.2);
11 | border-radius: 4px;
12 | margin: 0.5em;
13 | background: #eee;
14 | }
15 |
16 | .server-name {
17 | font-size: 2em;
18 | font-weight: bold;
19 | flex: 1;
20 | }
21 |
22 | .controls { align-self: flex-start; }
23 | }
24 |
25 | .server-footer {
26 | margin-bottom: 0.5em;
27 | text-align: center;
28 | opacity: 0.5;
29 | display: block;
30 | color: inherit;
31 | text-decoration: none;
32 |
33 | &:hover { opacity: 1; }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/SupClient/readFile.ts:
--------------------------------------------------------------------------------
1 | export default function readFile(file: File, type: string, callback: (err: Error, data?: any) => void) {
2 | const reader = new FileReader;
3 |
4 | reader.onload = (event) => {
5 | let data: any;
6 |
7 | if (type === "json") {
8 | try { data = JSON.parse((event.target as FileReader).result as string); }
9 | catch (err) { callback(err, null); return; }
10 | } else{
11 | data = (event.target as FileReader).result;
12 | }
13 |
14 | callback(null, data);
15 | };
16 |
17 | switch (type) {
18 | case "text":
19 | case "json":
20 | reader.readAsText(file);
21 | break;
22 |
23 | case "arraybuffer":
24 | reader.readAsArrayBuffer(file);
25 | break;
26 |
27 | default:
28 | callback(new Error(`Unsupported readFile type: ${type}`));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface BaseServer {
4 | data: any;
5 | io: SocketIO.Namespace;
6 |
7 | removeRemoteClient(socketId: string): void;
8 | }
9 |
10 | declare module "passport.socketio" {
11 | interface AuthorizeOptions {
12 | passport?: any;
13 | key?: string;
14 | secret?: string;
15 | store?: any;
16 | cookieParser?: any;
17 | success?: (data: any, accept: (error?: Error) => void) => void;
18 | fail?: (data: any, message: string, critical: boolean, accept: (error?: Error) => void) => void;
19 | }
20 |
21 | export function authorize(options: AuthorizeOptions): (socket: any, fn: (err?: any) => void) => void;
22 | }
23 |
24 | declare module "tsscmp" {
25 | function compare(a: string, b: string): boolean;
26 | namespace compare {}
27 | export = compare;
28 | }
29 |
--------------------------------------------------------------------------------
/client/build/index.styl:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | flex-flow: column;
4 | align-items: stretch;
5 | padding: 2em;
6 | background: #eee;
7 | }
8 |
9 | header {
10 | text-transform: uppercase;
11 | font-size: 1.5em;
12 | margin-bottom: .5em;
13 | overflow: hidden;
14 | white-space: nowrap;
15 | text-overflow: ellipsis;
16 | }
17 |
18 | progress {
19 | width: 100%;
20 | }
21 |
22 | .info {
23 | display: flex;
24 | margin-top: 1em;
25 | }
26 |
27 | .status {
28 | flex: 1;
29 | color: #666;
30 | text-align: left;
31 | overflow: hidden;
32 | white-space: nowrap;
33 | text-overflow: ellipsis;
34 | }
35 |
36 | .details {
37 | margin-top: 1em;
38 | border: 1px solid #aaa;
39 | background: #fff;
40 | overflow-y: scroll;
41 | flex: 1 1 0;
42 | }
43 |
44 | .details ol {
45 | list-style: none;
46 | margin: 0;
47 | padding: 0.5em;
48 | }
--------------------------------------------------------------------------------
/SupClient/styles/reset.styl:
--------------------------------------------------------------------------------
1 | @import "./Roboto"
2 | @import "./buttons"
3 |
4 | * {
5 | box-sizing border-box;
6 | min-width: 0;
7 | min-height: 0;
8 | font-family: inherit;
9 | }
10 |
11 | *[hidden] {
12 | display: none !important;
13 | }
14 |
15 | html, body {
16 | height: 100%;
17 | }
18 |
19 | body {
20 | margin: 0;
21 | -webkit-user-select: none;
22 | -moz-user-select: none;
23 | user-select: none;
24 | cursor: default;
25 | font-family: "Roboto", sans-serif;
26 | font-size: 14px;
27 | }
28 |
29 | .superpowers-error {
30 | position: absolute;
31 | top: 0;
32 | left: 0;
33 | right: 0;
34 | bottom: 0;
35 | background: #eee;
36 | padding: 2em;
37 | color: #444;
38 |
39 | h1 { margin-bottom: 1em; }
40 | }
41 |
42 | input[type=number]:not(:hover):not(:focus) {
43 | -moz-appearance: textfield;
44 | }
45 |
46 | input:invalid { outline-color: red; }
47 |
--------------------------------------------------------------------------------
/client/login/index.ts:
--------------------------------------------------------------------------------
1 | const port = (window.location.port.length === 0) ? (window.location.protocol === "https" ? "443" : "80") : window.location.port;
2 |
3 | const connectingElt = document.querySelector(".connecting") as HTMLDivElement;
4 | const formElt = document.querySelector(".login") as HTMLDivElement;
5 |
6 | formElt.hidden = true;
7 |
8 | let serverName: string;
9 |
10 | SupClient.fetch("superpowers.json", "json", (err, serverInfo) => {
11 | serverName = serverInfo.serverName;
12 | SupClient.i18n.load([{ root: "/", name: "hub" }, { root: "/", name: "login" }], start);
13 | });
14 |
15 | function start() {
16 | if (serverName == null) serverName = SupClient.i18n.t(`hub:serverAddress`, { hostname: window.location.hostname, port });
17 | document.querySelector(".server-name").textContent = serverName;
18 |
19 | formElt.hidden = false;
20 | connectingElt.hidden = true;
21 | }
22 |
--------------------------------------------------------------------------------
/public/locales/en/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Hub",
3 | "serverAddress": "${hostname} on port ${port}",
4 | "newProject": {
5 | "noSystemError": {
6 | "header": "Can't create a project",
7 | "message": "This server doesn't have any systems installed yet. Please install one from the Server Settings tab first."
8 | },
9 | "title": "New project",
10 | "prompt": "Enter a name and select a type for the new project.",
11 | "namePlaceholder": "Project name",
12 | "descriptionPlaceholder": "Description (optional)",
13 | "emptyProject": {
14 | "title": "Empty project",
15 | "description": "An empty project to start from scratch."
16 | },
17 | "autoOpen": "Open after creation"
18 | },
19 | "editDetails": {
20 | "title": "Edit details",
21 | "prompt": "Edit the project's details."
22 | },
23 | "openProject": "Open project",
24 | "language": "Language"
25 | }
26 |
--------------------------------------------------------------------------------
/public/locales/fr/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "serverAddress": "${hostname} sur le port ${port}",
3 | "newProject": {
4 | "noSystemError": {
5 | "header": "Impossible de créer un projet",
6 | "message": "Ce serveur n'a aucun système installé. Veuillez d'abord en installer un depuis l'onglet de paramètrage du serveur."
7 | },
8 | "title": "Nouveau projet",
9 | "prompt": "Entrez le nom et choisissez le type du nouveau projet.",
10 | "namePlaceholder": "Nom du projet",
11 | "descriptionPlaceholder": "Description (optionnelle)",
12 | "emptyProject": {
13 | "title": "Projet vide",
14 | "description": "Un projet vide pour partir de zéro."
15 | },
16 | "autoOpen": "Ouvrir après création"
17 | },
18 | "editDetails": {
19 | "title": "Éditer les informations",
20 | "prompt": "Éditez les informations du projet."
21 | },
22 | "openProject": "Ouvrir le projet",
23 | "language": "Langue"
24 | }
25 |
--------------------------------------------------------------------------------
/public/locales/pt-BR/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Hub",
3 | "serverAddress": "${hostname} na porta ${port}",
4 | "newProject": {
5 | "noSystemError": {
6 | "header": "Não é possível criar o projeto",
7 | "message": "O servidor não possui sistema algum instalado ainda. Por favor, instale algum através da aba Configurações do Servidor."
8 | },
9 | "title": "Novo projeto",
10 | "prompt": "Insira um nome e selecione um tipo para o novo projeto.",
11 | "namePlaceholder": "Nome do projeto",
12 | "descriptionPlaceholder": "Descrição (opcional)",
13 | "emptyProject": {
14 | "title": "Projeto vazio",
15 | "description": "Um projeto vazio para começar do zero."
16 | },
17 | "autoOpen": "Abrir o projeto após criá-lo"
18 | },
19 | "editDetails": {
20 | "title": "Alterar detalhes",
21 | "prompt": "Alterar os detalhes do projeto."
22 | },
23 | "openProject": "Abrir projeto",
24 | "language": "Idioma"
25 | }
--------------------------------------------------------------------------------
/SupCore/gulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const gulp = require("gulp");
4 |
5 | // TypeScript
6 | const ts = require("gulp-typescript");
7 | const tsProject = ts.createProject("./tsconfig.json");
8 | const tslint = require("gulp-tslint");
9 |
10 | gulp.task("typescript", () => {
11 | const tsResult = tsProject.src()
12 | .pipe(tslint({ formatter: "prose" }))
13 | .pipe(tslint.report({ emitError: true }))
14 | .on("error", (err) => { throw err; })
15 | .pipe(tsProject())
16 | return tsResult.js.pipe(gulp.dest("./"));
17 | });
18 |
19 | // Browserify
20 | const browserify = require("browserify");
21 | const source = require("vinyl-source-stream");
22 | gulp.task("browserify", gulp.series("typescript", () =>
23 | browserify("./index.js", { standalone: "SupCore" })
24 | .bundle()
25 | .pipe(source("SupCore.js"))
26 | .pipe(gulp.dest("../public"))
27 | ));
28 |
29 | // All
30 | gulp.task("default", gulp.series("typescript", "browserify"));
31 |
--------------------------------------------------------------------------------
/public/locales/it/hub.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Hub",
3 | "serverAddress": "${hostname} sulla porta ${port}",
4 | "newProject": {
5 | "noSystemError": {
6 | "header": "Impossibile creare il progetto",
7 | "message": "Questo server non ha ancora nessun sistema installato. Installarne uno dal pannello delle Impostazioni del Server."
8 | },
9 | "title": "Nuovo progetto",
10 | "prompt": "Inserici un nome e seleziona il tipo di progetto.",
11 | "namePlaceholder": "Nome del progetto",
12 | "descriptionPlaceholder": "Descrizione (opzionale)",
13 | "emptyProject": {
14 | "title": "Progetto vuoto",
15 | "description": "Un progetto vuoto per incominciare da zero."
16 | },
17 | "autoOpen": "Apri dopo la creazione"
18 | },
19 | "editDetails": {
20 | "title": "Modifica i dettagli",
21 | "prompt": "Modifica i dettagli del progetto."
22 | },
23 | "openProject": "Apri il progetto",
24 | "language": "Lingua"
25 | }
26 |
--------------------------------------------------------------------------------
/client/project/sidebar/index.ts:
--------------------------------------------------------------------------------
1 | import * as ResizeHandle from "resize-handle";
2 |
3 | import * as entriesTreeView from "./entriesTreeView";
4 | import * as header from "./header";
5 |
6 | export const openInNewWindowButton = SupClient.html("button", "open-in-new-window", { title: SupClient.i18n.t("project:treeView.openInNewWindow") });
7 |
8 | export function start() {
9 | const sidebarResizeHandle = new ResizeHandle(document.querySelector(".sidebar") as HTMLElement, "left");
10 | if (SupClient.query.asset != null || SupClient.query["tool"] != null) {
11 | sidebarResizeHandle.handleElt.classList.add("collapsed");
12 | sidebarResizeHandle.targetElt.style.width = "0";
13 | sidebarResizeHandle.targetElt.style.display = "none";
14 | }
15 |
16 | header.start();
17 | entriesTreeView.start();
18 | }
19 |
20 | export function enable() {
21 | header.enable();
22 | entriesTreeView.enable();
23 | }
24 |
25 | export function disable() {
26 | header.disable();
27 | entriesTreeView.disable();
28 | }
29 |
--------------------------------------------------------------------------------
/SupCore/Data/Resources.ts:
--------------------------------------------------------------------------------
1 | import * as SupData from "./index";
2 | import * as path from "path";
3 |
4 | // Plugin resources are assets managed by plugins outside the project's asset tree
5 | // They might be used for project-wide plugin-specific settings for instance
6 | export default class Resources extends SupData.Base.Dictionary {
7 | constructor(public server: ProjectServer) {
8 | super();
9 | }
10 |
11 | acquire(id: string, owner: SupCore.RemoteClient, callback: (err: Error, item: SupCore.Data.Base.Resource) => void) {
12 | if (this.server.system.data.resourceClasses[id] == null) { callback(new Error(`Invalid resource id: ${id}`), null); return; }
13 |
14 | super.acquire(id, owner, callback);
15 | }
16 |
17 | _load(id: string) {
18 | const resourceClass = this.server.system.data.resourceClasses[id];
19 |
20 | const resource = new resourceClass(id, null, this.server);
21 | resource.load(path.join(this.server.projectPath, `resources/${id}`));
22 |
23 | return resource;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/commands/registry.ts:
--------------------------------------------------------------------------------
1 | import * as utils from "./utils";
2 |
3 | export default function showRegistry() {
4 | utils.getRegistry((err, registry) => {
5 | if (err != null) {
6 | if (process != null && process.send != null) process.send({ type: "registry", error: err.message });
7 | console.log("Could not get registry:");
8 | throw err;
9 | }
10 |
11 | if (process != null && process.send != null) process.send({ type: "registry", registry });
12 |
13 | console.log(`Core - Latest: v${registry.core.version} / Installed: v${registry.core.localVersion}`);
14 | console.log("");
15 |
16 | for (const systemId in registry.systems) {
17 | const system = registry.systems[systemId];
18 | const local = system.localVersion != null ? `Installed: v${system.localVersion}` : "Not Installed";
19 | console.log(`System "${systemId}" - Latest: v${system.version} / ${local}`);
20 | utils.listAvailablePlugins(registry, systemId);
21 | console.log("");
22 | }
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/public/locales/zh-TW/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "回專案列表",
4 | "publish": "發布專案",
5 | "publishDisabled": "發佈專案 (由於技術因素,只能用在 Superpowers app)",
6 | "stop": "停止專案",
7 | "debug": "除錯專案 (F6)",
8 | "run": "執行專案 (F5)",
9 | "notifications": {
10 | "enable": "開啟通知",
11 | "disable": "關閉通知",
12 | "new": "新聊天訊息在 \"${projectName}\" 專案"
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "在新視窗開啟",
17 | "newAsset": {
18 | "title": "新素材",
19 | "prompt": "選擇新素材的類別並輸入名稱",
20 | "placeholder": "素材名稱 (非必填)",
21 | "openAfterCreation": "建立後開啟"
22 | },
23 | "newFolder": {
24 | "title": "新資料夾",
25 | "prompt": "請輸入新資料夾的名稱",
26 | "placeholder": "輸入名稱",
27 | "initialValue": "資料夾"
28 | },
29 | "renamePrompt": "輸入此素材的新名稱",
30 | "duplicatePrompt": "輸入新素材的名稱",
31 | "trash": {
32 | "title": "移至垃圾桶",
33 | "prompt": "你確定要將這些素材移至垃圾桶?",
34 | "warnBrokenDependency": "${entryName} 仍使用在 ${dependentEntryNames}."
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/locales/zh-TW/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "繁體中文",
3 |
4 | "namePatternDescription": "以下字元無法使用: \\, /, :, *, ?, \", <, >, |, [ and ].",
5 | "none": "(None)",
6 |
7 | "findAsset": {
8 | "placeholder": "搜尋素材"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "更新",
13 | "applyChangesWithErrors": "忽略錯誤並更新",
14 | "cancel": "取消",
15 | "close": "關閉",
16 | "create": "建立",
17 | "delete": "刪除",
18 | "download": "下載",
19 | "duplicate": "複製",
20 | "new": "開新檔案",
21 | "open": "開啟",
22 | "rename": "更改名稱",
23 | "save": "儲存",
24 | "search": "搜尋",
25 | "skip": "略過",
26 | "update": "更新",
27 | "upload": "上傳",
28 |
29 | "cut": "剪下",
30 | "copy": "複製",
31 | "paste": "貼上"
32 | },
33 |
34 | "states": {
35 | "disabled": "已關閉",
36 | "enabled": "已開啟",
37 | "loading": "讀取中...",
38 | "connecting": "連線中...",
39 | "saving": "儲存中..."
40 | },
41 |
42 | "hotkeys": {
43 | "control": "Ctrl",
44 | "command": "Cmd",
45 | "shift": "Shift",
46 | "delete": "Del",
47 | "return": "Return"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Superpowers is distributed under the ISC license,
2 | in the hope of making it as useful as possible for everyone.
3 | https://en.wikipedia.org/wiki/ISC_license
4 |
5 | We are a welcoming community and we'd love to have you contributing!
6 | https://github.com/superpowers
7 |
8 | ------------------------------------------------------------------------------
9 |
10 | Copyright © 2014-2016, Sparklin Labs
11 |
12 | Permission to use, copy, modify, and/or distribute this software for any
13 | purpose with or without fee is hereby granted, provided that the above
14 | copyright notice and this permission notice appear in all copies.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
17 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
18 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
19 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
20 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
21 | OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
22 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
23 |
--------------------------------------------------------------------------------
/public/locales/ja/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "日本語",
3 |
4 | "namePatternDescription": "次の文字は使えません: \\, /, :, *, ?, \", <, >, |, [ and ].",
5 | "none": "(None)",
6 |
7 | "findAsset": {
8 | "placeholder": "アセットの検索"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Apply changes",
13 | "applyChangesWithErrors": "Apply changes with errors",
14 | "cancel": "キャンセル",
15 | "close": "閉じる",
16 | "create": "作成",
17 | "delete": "削除",
18 | "download": "ダウンロード",
19 | "duplicate": "複製",
20 | "filter": "フィルター",
21 | "new": "新規",
22 | "open": "開く",
23 | "rename": "リネーム",
24 | "save": "保存",
25 | "search": "検索",
26 | "skip": "スキップ",
27 | "update": "更新",
28 | "upload": "アップロード",
29 |
30 | "cut": "カット",
31 | "copy": "コピー",
32 | "paste": "ペースト"
33 | },
34 |
35 | "states": {
36 | "disabled": "無効化",
37 | "enabled": "有効化",
38 | "loading": "ロード中...",
39 | "connecting": "接続中...",
40 | "saving": "保存中..."
41 | },
42 |
43 | "hotkeys": {
44 | "control": "Ctrl",
45 | "command": "Cmd",
46 | "shift": "Shift",
47 | "delete": "Del",
48 | "return": "Return"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/SupClient/styles/toolbar.styl:
--------------------------------------------------------------------------------
1 | .toolbar {
2 | background-color: #eee;
3 | background-image: url(/images/toolbar/background.png);
4 | display: flex;
5 | align-items: center;
6 | flex-wrap: wrap;
7 | white-space: nowrap;
8 | overflow: hidden;
9 | margin-bottom: -1px;
10 |
11 | > div {
12 | display: flex;
13 | align-items: center;
14 | margin-bottom: 1px;
15 | }
16 |
17 | > div:not(:last-of-type) {
18 | padding-right: 0.5em;
19 | }
20 |
21 | > div > div {
22 | height: 30px;
23 | display: flex;
24 | align-items: center;
25 | padding-right: 0.25em;
26 | flex: auto 0 0;
27 | }
28 |
29 | > div > .title {
30 | text-transform: uppercase;
31 | color: #666;
32 | padding: 0.5em;
33 | margin-right: 0.25em;
34 | background: #ddd;
35 | }
36 |
37 | > div > div:not(.radio-strip):not(.button-strip) {
38 | & > label, & > select {
39 | margin-right: 0.5em;
40 | &:first-child { margin-left: 0.25em; }
41 | }
42 |
43 | & > button:not(:last-child) {
44 | margin-right: 0.25em;
45 | }
46 | }
47 |
48 | input[type=number] { width: 50px; }
49 | input[type=color] { margin-right: 0.5em; }
50 | }
51 |
--------------------------------------------------------------------------------
/public/locales/ja/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "ハブに戻る",
4 | "publish": "プロジェクトを発行",
5 | "publishDisabled": "プロジェクトを発行 (これは、技術的理由によりSuperpowers appでのみ動きます)",
6 | "stop": "プロジェクトの停止",
7 | "debug": "プロジェクトのデバッグ (F6)",
8 | "run": "プロジェクトの実行 (F5)",
9 | "notifications": {
10 | "enable": "クリックで通知が有効化",
11 | "disable": "クリックで通知が無効化",
12 | "new": "\"${projectName}\" プロジェクトに新しいチャットメッセージ"
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "新しいウィンドウで開く",
17 | "newAsset": {
18 | "title": "新しいアセット",
19 | "prompt": "タイプを選択して新しいアセットの名前を入力してください。",
20 | "placeholder": "アセット名 (オプション)",
21 | "openAfterCreation": "作成後にすぐ開く"
22 | },
23 | "newFolder": {
24 | "title": "新しいフォルダー",
25 | "prompt": "新しいフォルダーの名前を入力してください。",
26 | "placeholder": "フォルダー名",
27 | "initialValue": "フォルダー"
28 | },
29 | "renamePrompt": "アセットの新しい名前を入力してください。",
30 | "duplicatePrompt": "アセットの新しい名前を入力してください。",
31 | "trash": {
32 | "title": "削除",
33 | "prompt": "選択したものをゴミ箱に移動しますがよろしいですか?",
34 | "warnBrokenDependency": "${entryName} は ${dependentEntryNames} で使われています。"
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/SupCore/Data/Projects.ts:
--------------------------------------------------------------------------------
1 | import ListById from "./Base/ListById";
2 | import * as _ from "lodash";
3 |
4 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
5 |
6 | export default class Projects extends ListById {
7 | static schema: SupCore.Data.Schema = {
8 | name: { type: "string", minLength: 1, maxLength: 80 },
9 | description: { type: "string", maxLength: 300 },
10 | formatVersion: { type: "number?" },
11 | systemId: { type: "string" }
12 | };
13 |
14 | static sort(a: SupCore.Data.ProjectManifestPub, b: SupCore.Data.ProjectManifestPub) {
15 | return a.name.localeCompare(b.name);
16 | }
17 |
18 | pub: SupCore.Data.ProjectManifestPub[];
19 | byId: { [id: string]: SupCore.Data.ProjectManifestPub; };
20 |
21 | constructor(pub: SupCore.Data.ProjectManifestPub[]) {
22 | super(pub, Projects.schema);
23 | this.generateNextId = this.generateProjectId;
24 | }
25 |
26 | private generateProjectId = () => {
27 | let id: string = null;
28 |
29 | while (true) {
30 | id = "";
31 | for (let i = 0; i < 4; i++) id += _.sample(characters);
32 | if (this.byId[id] == null) break;
33 | }
34 |
35 | return id;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "indent": [
4 | true,
5 | "spaces"
6 | ],
7 | "eofline": true,
8 | "semicolon": true,
9 | "max-line-length": [
10 | true,
11 | 200
12 | ],
13 | "class-name": true,
14 | "variable-name": true,
15 | "comment-format": [
16 | true,
17 | "check-space"
18 | ],
19 | "no-trailing-whitespace": true,
20 | "no-unused-expression": [
21 | true,
22 | "allow-new"
23 | ],
24 | "no-var-requires": true,
25 | "quotemark": [
26 | true,
27 | "double"
28 | ],
29 | "radix": true,
30 | "triple-equals": [
31 | true,
32 | "allow-null-check"
33 | ],
34 | "whitespace": [
35 | true,
36 | "check-branch",
37 | "check-decl",
38 | "check-operator",
39 | "check-separator",
40 | "check-type",
41 | "check-module"
42 | ],
43 | "no-construct": true,
44 | "no-empty": true,
45 | "no-switch-case-fall-through": true,
46 | "no-var-keyword": true,
47 | "no-duplicate-variable": true,
48 | "no-eval": true,
49 | "no-internal-module": true,
50 | "no-require-imports": true,
51 | "member-access": false
52 | }
53 | }
--------------------------------------------------------------------------------
/SupClient/gulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const gulp = require("gulp");
4 |
5 | // TypeScript
6 | const ts = require("gulp-typescript");
7 | const tsProject = ts.createProject("./tsconfig.json");
8 | const tslint = require("gulp-tslint");
9 |
10 | gulp.task("typescript", () => {
11 | const tsResult = tsProject.src()
12 | .pipe(tslint({ formatter: "prose" }))
13 | .pipe(tslint.report({ emitError: true }))
14 | .on("error", (err) => { throw err; })
15 | .pipe(tsProject())
16 | return tsResult.js.pipe(gulp.dest("./"));
17 | });
18 |
19 | // Stylus
20 | const stylus = require("gulp-stylus");
21 | gulp.task("stylus", function() {
22 | return gulp.src("./styles/*.styl").pipe(stylus({ errors: true, compress: true })).pipe(gulp.dest("../public/styles"));
23 | });
24 |
25 | // Browserify
26 | const browserify = require("browserify");
27 | const source = require("vinyl-source-stream");
28 | gulp.task("browserify", gulp.series("typescript", () =>
29 | browserify("./index.js", { standalone: "SupClient" })
30 | .transform("brfs").bundle()
31 | .pipe(source("SupClient.js"))
32 | .pipe(gulp.dest("../public"))
33 | ));
34 |
35 | // All
36 | gulp.task("default", gulp.parallel("stylus", gulp.series("typescript", "browserify")));
37 |
--------------------------------------------------------------------------------
/public/locales/th/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "ภาษาไทย",
3 |
4 | "namePatternDescription": "ห้ามใช้ตัวอักษรต่อไปนี้ : \\, /, :, *, ?, \", <, >, |, [ และ ]",
5 | "none": "(ปกติ)",
6 |
7 | "findAsset": {
8 | "placeholder": "ค้นหาทรัพยากร"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "นำไปใช้",
13 | "applyChangesWithErrors": "นำไปใช้ด้วยข้อผิดพลาด",
14 | "cancel": "ยกเลิก",
15 | "close": "ปิด",
16 | "create": "สร้างใหม่",
17 | "delete": "ลบ",
18 | "download": "ดาวน์โหลด",
19 | "duplicate": "ทำซ้ำ",
20 | "new": "สร้างใหม่",
21 | "open": "เปิด",
22 | "rename": "เปลี่ยนชื่อ",
23 | "save": "บันทึก",
24 | "search": "ค้นหา",
25 | "skip": "ข้าม",
26 | "update": "อัพเดท",
27 | "upload": "อัพโหลด",
28 |
29 | "cut": "ตัด",
30 | "copy": "คัดลอก",
31 | "paste": "วาง"
32 | },
33 |
34 | "states": {
35 | "disabled": "ปิดการทำงาน",
36 | "enabled": "เปิดการทำงาน",
37 | "loading": "กำลังโหลด...",
38 | "connecting": "กำลังเชื่อมต่อ...",
39 | "saving": "กำลังบันทึก..."
40 | },
41 |
42 | "hotkeys": {
43 | "control": "Ctrl",
44 | "command": "Cmd",
45 | "shift": "Shift",
46 | "delete": "Del",
47 | "return": "Return"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/locales/ca/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Català",
3 |
4 | "namePatternDescription": "Els següents caràcters no es poden fer servir: \\, /, :, *, ?, \", <, >, |, [ i ].",
5 | "none": "(Ningún)",
6 |
7 | "findAsset": {
8 | "placeholder": "Buscar assets"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Aplicar canvis",
13 | "applyChangesWithErrors": "Aplicar canvis amb errors",
14 | "cancel": "Cancelar",
15 | "close": "Tancar",
16 | "create": "Crear",
17 | "delete": "Eliminar",
18 | "download": "Descarregar",
19 | "duplicate": "Duplicar",
20 | "new": "Nou",
21 | "open": "Obrir",
22 | "rename": "Renombrar",
23 | "save": "Guardar",
24 | "search": "Buscar",
25 | "skip": "Saltar",
26 | "update": "Actualitzar",
27 | "upload": "Pujar",
28 |
29 | "cut": "Tallar",
30 | "copy": "Copiar",
31 | "paste": "Enganxar"
32 | },
33 |
34 | "states": {
35 | "disabled": "Deshabilitat",
36 | "enabled": "Habilitat",
37 | "loading": "Carregant...",
38 | "connecting": "Conectant...",
39 | "saving": "Guardant..."
40 | },
41 |
42 | "hotkeys": {
43 | "control": "Ctrl",
44 | "command": "Cmd",
45 | "shift": "Shift",
46 | "delete": "Del",
47 | "return": "Return"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/locales/es/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Español",
3 |
4 | "namePatternDescription": "Los siguientes carácteres no pueden ser usados: \\, /, :, *, ?, \", <, >, |, [ y ].",
5 | "none": "(Ninguno)",
6 |
7 | "findAsset": {
8 | "placeholder": "Buscar assets"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Aplicar cambios",
13 | "applyChangesWithErrors": "Aplicar cambios con errores",
14 | "cancel": "Cancelar",
15 | "close": "Cerrar",
16 | "create": "Crear",
17 | "delete": "Eliminar",
18 | "download": "Descargar",
19 | "duplicate": "Duplicar",
20 | "new": "Nuevo",
21 | "open": "Abrir",
22 | "rename": "Renombrar",
23 | "save": "Guardar",
24 | "search": "Buscar",
25 | "skip": "Saltar",
26 | "update": "Actualizar",
27 | "upload": "Subir",
28 |
29 | "cut": "Cortar",
30 | "copy": "Copiar",
31 | "paste": "Pegar"
32 | },
33 |
34 | "states": {
35 | "disabled": "Deshabilitado",
36 | "enabled": "Habilitado",
37 | "loading": "Cargando...",
38 | "connecting": "Conectando...",
39 | "saving": "Guardando..."
40 | },
41 |
42 | "hotkeys": {
43 | "control": "Ctrl",
44 | "command": "Cmd",
45 | "shift": "Shift",
46 | "delete": "Del",
47 | "return": "Return"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/locales/sv/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Svenska",
3 |
4 | "namePatternDescription": "Följande tecken kan ej användas: \\, /, :, *, ?, \", <, >, |, [ and ].",
5 | "none": "(Inget)",
6 |
7 | "findAsset": {
8 | "placeholder": "Sök resurser"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Spara ändringar",
13 | "applyChangesWithErrors": "Spara ändringar med fel",
14 | "cancel": "Avbryt",
15 | "close": "Stäng",
16 | "create": "Skapa",
17 | "delete": "Radera",
18 | "download": "Ladda ner",
19 | "duplicate": "Duplicera",
20 | "filter": "Filtrera",
21 | "new": "Ny",
22 | "open": "Öppna",
23 | "rename": "Ändra namn",
24 | "save": "Spara",
25 | "search": "Sök",
26 | "skip": "Hoppa över",
27 | "update": "Uppdatera",
28 | "upload": "Ladda upp",
29 |
30 | "cut": "Klipp ut",
31 | "copy": "Kopiera",
32 | "paste": "Klistra in"
33 | },
34 |
35 | "states": {
36 | "disabled": "Avaktiverad",
37 | "enabled": "Aktiverad",
38 | "loading": "Laddar...",
39 | "connecting": "Kopplar upp...",
40 | "saving": "Sparar..."
41 | },
42 |
43 | "hotkeys": {
44 | "control": "Ctrl",
45 | "command": "Cmd",
46 | "shift": "Skift",
47 | "delete": "Del",
48 | "return": "Retur"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/SupClient/html.ts:
--------------------------------------------------------------------------------
1 | const specialOptionKeys = [ "parent", "style", "dataset" ];
2 |
3 | export default function html(tag: string, classList?: string|string[]|SupClient.HTMLOptions, options?: SupClient.HTMLOptions) {
4 | if (options == null) {
5 | if (typeof classList === "object" && !Array.isArray(classList)) {
6 | options = classList;
7 | classList = null;
8 | } else {
9 | options = {};
10 | }
11 | }
12 | if (typeof classList === "string") classList = [ classList ] as any;
13 |
14 | const elt = document.createElement(tag);
15 | if (classList != null) {
16 | // NOTE: `elt.classList.add.apply(elt, classList);`
17 | // throws IllegalInvocationException at least in Chrome
18 | for (const name of classList as string[]) elt.classList.add(name);
19 | }
20 |
21 | for (const key in options) {
22 | if (specialOptionKeys.indexOf(key) !== -1) continue;
23 | const value = (options as any)[key];
24 | (elt as any)[key] = value;
25 | }
26 |
27 | if (options.parent != null) options.parent.appendChild(elt);
28 | if (options.style != null) for (const key in options.style) (elt.style as any)[key] = (options.style as any)[key];
29 | if (options.dataset != null) for (const key in options.dataset) elt.dataset[key] = options.dataset[key];
30 |
31 | return elt;
32 | }
33 |
--------------------------------------------------------------------------------
/SupCore/ProjectServer.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ProjectServer {
4 | io: SocketIO.Namespace;
5 | system: SupCore.System;
6 |
7 | data: ProjectServerData;
8 | projectPath: string;
9 |
10 | addEntry(clientSocketId: string, name: string, type: string, options: any, callback: (err: string, newId?: string) => any): void;
11 | duplicateEntry(clientSocketId: string, newName: string, originalEntryId: string, options: any, callback: (err: string, duplicatedId?: string) => any): void;
12 | moveEntry(clientSocketId: string, entryId: string, parentId: string, index: number, callback: (err: string) => any): void;
13 | trashEntry(clientSocketId: string, entryId: string, callback: (err: string) => any): void;
14 | renameEntry(clientSocketId: string, entryId: string, name: string, callback: (err: string) => any): void;
15 | saveEntry(clientSocketId: string, entryId: string, revisionName: string, callback: (err: string) => void): void;
16 |
17 | moveAssetFolderToTrash(trashedAssetFolder: string, callback: (err: Error) => any): void;
18 | }
19 |
20 | interface ProjectServerData {
21 | manifest: SupCore.Data.ProjectManifest;
22 | entries: SupCore.Data.Entries;
23 |
24 | assets: SupCore.Data.Assets;
25 | rooms: SupCore.Data.Rooms;
26 | resources: SupCore.Data.Resources;
27 | }
28 |
--------------------------------------------------------------------------------
/public/locales/de/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Deutsch",
3 |
4 | "namePatternDescription": "Die folgenden Zeichen dürfen nicht verwendet werden: \\, /, :, *, ?, \", <, >, |, [ und ].",
5 | "none": "(Keine)",
6 |
7 | "findAsset": {
8 | "placeholder": "Suche nach Assets"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Änderungen anwenden",
13 | "applyChangesWithErrors": "Änderungen mit Fehlern anwenden",
14 | "cancel": "Abbrechen",
15 | "close": "Schließen",
16 | "create": "Erstellen",
17 | "delete": "Löschen",
18 | "download": "Download",
19 | "duplicate": "Duplizieren",
20 | "new": "Neu",
21 | "open": "Öffnen",
22 | "rename": "Umbenennen",
23 | "save": "Speichern",
24 | "search": "Suchen",
25 | "skip": "Überspringen",
26 | "update": "Aktualisieren",
27 | "upload": "Hochladen",
28 |
29 | "cut": "Ausschneiden",
30 | "copy": "Kopieren",
31 | "paste": "Einfügen"
32 | },
33 |
34 | "states": {
35 | "disabled": "Deaktiviert",
36 | "enabled": "Aktiviert",
37 | "loading": "Lade...",
38 | "connecting": "Verbinde...",
39 | "saving": "Speichere..."
40 | },
41 |
42 | "hotkeys": {
43 | "control": "Strg",
44 | "command": "Cmd",
45 | "shift": "Umschalt",
46 | "delete": "Entfernen",
47 | "return": "Enter"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/locales/fr/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Français",
3 |
4 | "namePatternDescription": "Les caractères suivants ne peuvent pas être utilisés : \\, /, :, *, ?, \", <, >, |, [ et ].",
5 | "none": "(Vide)",
6 |
7 | "findAsset": {
8 | "placeholder": "Rechercher des assets"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Appliquer les modifications",
13 | "applyChangesWithErrors": "Appliquer les modifications avec erreurs",
14 | "cancel": "Annuler",
15 | "close": "Fermer",
16 | "create": "Créer",
17 | "delete": "Supprimer",
18 | "download": "Télécharger",
19 | "duplicate": "Dupliquer",
20 | "new": "Nouveau",
21 | "open": "Ouvrir",
22 | "rename": "Renommer",
23 | "save": "Sauvegarder",
24 | "search": "Chercher",
25 | "skip": "Ignorer",
26 | "update": "Mettre à jour",
27 | "upload": "Uploader",
28 |
29 | "cut": "Couper",
30 | "copy": "Copier",
31 | "paste": "Coller"
32 | },
33 |
34 | "states": {
35 | "disabled": "Désactivé",
36 | "enabled": "Activé",
37 | "loading": "Chargement...",
38 | "connecting": "Connexion...",
39 | "saving": "Sauvegarde..."
40 | },
41 |
42 | "hotkeys": {
43 | "control": "Ctrl",
44 | "command": "Cmd",
45 | "shift": "Maj",
46 | "delete": "Suppr",
47 | "return": "Retour"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/locales/th/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "กลับไปที่ฮับ",
4 | "publish": "แพ็คโปรเจค",
5 | "publishDisabled": "แพ็คโปรเจค (ใช้งานได้เฉพาะบนโปรแกรม Superpowers เท่านั้นเนื่องจากเหตุผลทางเทคนิค)",
6 | "stop": "หยุดการทำงานของโปรเจค",
7 | "debug": "ดีบัคโปรเจค (F6)",
8 | "run": "สั่งโปรเจคทำงาน (F5)",
9 | "notifications": {
10 | "enable": "คลิกเพื่อเปิดการแจ้งเตือน",
11 | "disable": "คลิกเพื่อปิดการแจ้งเตือน",
12 | "new": "มีข้อความใหม่ในโปรเจค \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "เปิดในหน้าต่างใหม่",
17 | "newAsset": {
18 | "title": "สร้างทรัพยากรใหม่",
19 | "prompt": "เลือกประเภทและกรอกชื่อสำหรับทรัพยากร",
20 | "placeholder": "ชื่อทรัพยากร (ตัวเลือก)",
21 | "openAfterCreation": "เปิดหลังจากสร้าง"
22 | },
23 | "newFolder": {
24 | "title": "สร้างแฟ้มใหม่",
25 | "prompt": "กรอกชื่อสำหรับแฟ้มใหม่",
26 | "placeholder": "กรอกชื่อ",
27 | "initialValue": "แฟ้ม"
28 | },
29 | "renamePrompt": "กรอกชื่อสำหรับทรัพยากร",
30 | "duplicatePrompt": "กรอกชื่อสำหรับทรัพยากรใหม่",
31 | "trash": {
32 | "title": "ทิ้งลงถังขยะ",
33 | "prompt": "คุณแน่ใจหรือที่จะทิ้งสิ่งนี้ลงถังขยะ?",
34 | "warnBrokenDependency": "${entryName} ถูกใช้ใน ${dependentEntryNames}."
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js: "node"
4 | # Skip "npm install" since "npm run build" takes care of it below
5 | install: true
6 | cache:
7 | directories:
8 | - node_modules
9 | script:
10 | - npm run build
11 | notifications:
12 | webhooks:
13 | urls:
14 | - https://webhooks.gitter.im/e/44f5607466509af53a93
15 | on_success: change
16 | on_failure: always
17 | on_start: never
18 | before_deploy:
19 | - npm run package
20 | deploy:
21 | provider: releases
22 | api_key:
23 | secure: ihYZuq2hqd/dAYskD3jZ1L4TQq/HI3vj2PlL0mvOFi4Husu3HU7x3GjHcUiA1l8pRxIxmZE6hxGL73Lp0x3xnrnE2EDmccM3ZQ7FDoY0NHpVU3V6FK2SUOB923jHJ2mBwsSqOzK2/ZFlXddFiFsSx0K///JYopZsaEam17GWxopk4ANIeqjCgO1c9dslKYPOqSYeJeQPU/kEZB0dSz1Wyno5WiCDsmg1wszctsbhgX25NCRRMbn4R3MSxfNgHCo9L71FlRiM3u2mjFldhuVVmvGsNH0DIoFIpGuPpura0V0et7OTeD5Mv2OH6h3Py7KxmiT3nCx8+cQWgoLNO78y02c6Jplklo8VyumZTmACykcpeilheYSeouY89xNIY+HFRhBnoqFmwlM8kQG5hpg1ScQL43fqmWRTFEGrPOZwYtdC3KnkSkLBs755WjIO9/dHsxOmW47YQM08ce6IYgg9Xvrtu5ekB25ZTsPxbKSxYZ460gaaRNQbUHJHkyjmxJW9L4PYd92GBuPauUojtEC5UrOI6cvmKUQUtdhIQZn3QwKOiRX4XEWgkSefkjFmMWrJa01i7LzoMKGmBPzhpz9NlqKI03B1mYAVDBDgEzPM6hP+2ejNL2Wg+6loFWPjNCi2S0KVb5AcBs7HpwiLV6plRn998ymwQX6fsl2yn+DjC6Y=
24 | file_glob: true
25 | file: "packages/superpowers-core-*.zip"
26 | skip_cleanup: true
27 | on:
28 | repo: superpowers/superpowers-core
29 | tags: true
30 |
--------------------------------------------------------------------------------
/public/locales/sv/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "Gå till hub",
4 | "publish": "Publicera projekt",
5 | "publishDisabled": "Publicera projekt (fungerar bara i Superpowers app av tekniska skäl)",
6 | "stop": "Stoppa projekt",
7 | "debug": "Debugga projekt (F6)",
8 | "run": "Kör projekt (F5)",
9 | "notifications": {
10 | "enable": "Klicka för att activera notifikationer",
11 | "disable": "Klicka för att avaktivera notifikationer",
12 | "new": "Nytt chatmeddelande i \"${projectName}\" projektet"
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Öppna i nytt fönster",
17 | "newAsset": {
18 | "title": "Ny resurs",
19 | "prompt": "Välj typ och ge namn till resursen.",
20 | "placeholder": "Resursnamn (valfritt)",
21 | "openAfterCreation": "Öppna direkt"
22 | },
23 | "newFolder": {
24 | "title": "Ny mapp",
25 | "prompt": "Ge den nya mapppen ett namn.",
26 | "placeholder": "Fyll i namn",
27 | "initialValue": "Mapp"
28 | },
29 | "renamePrompt": "Välj ett nytt namn för resursen",
30 | "duplicatePrompt": "Välj ett nytt namn för resursen",
31 | "trash": {
32 | "title": "Soptunna",
33 | "prompt": "Är du säker på att du vill slänga valda poster?",
34 | "warnBrokenDependency": "${entryName} används i ${dependentEntryNames}."
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/SupCore/Data/Base/Hash.ts:
--------------------------------------------------------------------------------
1 | import * as base from "./index";
2 | import { EventEmitter } from "events";
3 |
4 | export default class Hash extends EventEmitter {
5 | constructor(public pub: any, public schema: SupCore.Data.Schema) {
6 | super();
7 | }
8 |
9 | setProperty(path: string, value: number|string|boolean, callback: (err: string, value?: any) => any) {
10 | const parts = path.split(".");
11 |
12 | let rule = this.schema[parts[0]];
13 | for (const part of parts.slice(1)) {
14 | rule = rule.properties[part];
15 | if (rule.type === "any") break;
16 | }
17 |
18 | if (rule == null) { callback(`Invalid key: ${path}`); return; }
19 | if (rule.type !== "any") {
20 | const violation = base.getRuleViolation(value, rule);
21 | if (violation != null) { callback(`Invalid value for ${path}: ${base.formatRuleViolation(violation)}`); return; }
22 | }
23 |
24 | let obj = this.pub;
25 | for (const part of parts.slice(0, parts.length - 1)) obj = obj[part];
26 | obj[parts[parts.length - 1]] = value;
27 |
28 | callback(null, value);
29 | this.emit("change");
30 | }
31 |
32 | client_setProperty(path: string, value: number|string|boolean) {
33 | const parts = path.split(".");
34 |
35 | let obj = this.pub;
36 | for (const part of parts.slice(0, parts.length - 1)) obj = obj[part];
37 | obj[parts[parts.length - 1]] = value;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/SupCore/Data/index.ts:
--------------------------------------------------------------------------------
1 | import * as Base from "./Base";
2 |
3 | import Projects from "./Projects";
4 |
5 | import ProjectManifest from "./ProjectManifest";
6 | import Badges from "./Badges";
7 | import Entries from "./Entries";
8 |
9 | import Assets from "./Assets";
10 | import Resources from "./Resources";
11 |
12 | import Rooms from "./Rooms";
13 | import Room from "./Room";
14 | import RoomUsers from "./RoomUsers";
15 |
16 | export {
17 | Base,
18 | Projects, ProjectManifest, Badges, Entries,
19 | Assets, Resources, Rooms, Room, RoomUsers
20 | };
21 |
22 | export function hasDuplicateName(id: string, name: string, siblings: Array<{ id: string; name: string; }>): boolean {
23 | for (const sibling of siblings) {
24 | if (sibling.id !== id && sibling.name === name) return true;
25 | }
26 | return false;
27 | }
28 |
29 | export function ensureUniqueName(id: string, name: string, siblings: Array<{ id: string; name: string; }>): string {
30 | name = name.trim();
31 | let candidateName = name;
32 | let nameNumber = 1;
33 |
34 | // Look for an already exiting number at the end of the name
35 | const matches = name.match(/\d+$/);
36 | if (matches != null) {
37 | name = name.substring(0, name.length - matches[0].length);
38 | nameNumber = parseInt(matches[0], 10);
39 | }
40 |
41 | while (hasDuplicateName(id, candidateName, siblings)) candidateName = `${name}${++nameNumber}`;
42 | return candidateName;
43 | }
44 |
--------------------------------------------------------------------------------
/public/locales/ca/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "Anar al hub",
4 | "publish": "Publicar projecte",
5 | "publishDisabled": "Publicar projecte (només funciona desde la app de Superpowers per raons técniques)",
6 | "stop": "Parar projecte",
7 | "debug": "Depurar projecte (F6)",
8 | "run": "Executar projecte (F5)",
9 | "notifications": {
10 | "enable": "Fes click per habilitar les notificacions",
11 | "disable": "Fes click per deshabilitar les notificacions",
12 | "new": "Nou missatge de xat en el projecte \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Obrir en una nova finestra",
17 | "newAsset": {
18 | "title": "Nou asset",
19 | "prompt": "Selecciona el tipus i escriu un nom per al nou asset.",
20 | "placeholder": "Nom del asset (opcional)",
21 | "openAfterCreation": "Obrir després de crear"
22 | },
23 | "newFolder": {
24 | "title": "Nova carpeta",
25 | "prompt": "Escriu un nom per a la nova carpeta.",
26 | "placeholder": "Escriu un nom",
27 | "initialValue": "Carpeta"
28 | },
29 | "renamePrompt": "Escriu un nom per al asset.",
30 | "duplicatePrompt": "Escriu un nom per al nou asset.",
31 | "trash": {
32 | "title": "Esborrar",
33 | "prompt": "Segur que vols esborrar els elements seleccionats?",
34 | "warnBrokenDependency": "${entryName} s'está fent servir en ${dependentEntryNames}."
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/public/locales/it/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Italiano",
3 |
4 | "namePatternDescription": "Non è possibile utilizzare i seguenti caratteri: \\, /, :, *, ?, \", <, >, |, [ and ].",
5 | "none": "(Nessuno)",
6 |
7 | "findAsset": {
8 | "placeholder": "Cerca asset"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Applica le modifiche",
13 | "applyChangesWithErrors": "Applica le modifiche con errori",
14 | "cancel": "Annulla",
15 | "clear": "Pulisci",
16 | "close": "Chiudi",
17 | "create": "Crea",
18 | "delete": "Elimina",
19 | "download": "Scarica",
20 | "duplicate": "Duplica",
21 | "export": "Esporta",
22 | "filter": "Filtra",
23 | "import": "Importa",
24 | "new": "Nuovo",
25 | "open": "Apri",
26 | "rename": "Rinomina",
27 | "restore": "Ripristina",
28 | "save": "Salva",
29 | "search": "Cerca",
30 | "select": "Seleziona",
31 | "skip": "Salta",
32 | "update": "Aggiorna",
33 | "upload": "Carica",
34 |
35 | "cut": "Taglia",
36 | "copy": "Copia",
37 | "paste": "Incolla"
38 | },
39 |
40 | "states": {
41 | "disabled": "Disabilitato",
42 | "enabled": "Abilitato",
43 | "loading": "In caricamento...",
44 | "connecting": "In connessione...",
45 | "saving": "Salvataggio..."
46 | },
47 |
48 | "hotkeys": {
49 | "control": "Ctrl",
50 | "command": "Cmd",
51 | "shift": "Shift",
52 | "delete": "Del",
53 | "return": "Return"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/locales/ru/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Русский",
3 |
4 | "namePatternDescription": "Не используйте следующие символы: \\, /, :, *, ?, \", <, >, |, [ и ].",
5 | "none": "(пусто)",
6 |
7 | "findAsset": {
8 | "placeholder": "Поиск по ассетам"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Применить изменения",
13 | "applyChangesWithErrors": "Применить изменения с ошибками",
14 | "cancel": "Отменить",
15 | "clear": "Очистить",
16 | "close": "Закрыть",
17 | "create": "Создать",
18 | "delete": "Удалить",
19 | "download": "Скачать",
20 | "duplicate": "Дублировать",
21 | "export": "Экспорт",
22 | "filter": "Фильтр",
23 | "import": "Импорт",
24 | "new": "Новый",
25 | "open": "Открыть",
26 | "rename": "Переименовать",
27 | "restore": "Восстановить",
28 | "save": "Сохранить",
29 | "search": "Поиск",
30 | "select": "Выбрать",
31 | "skip": "Пропустить",
32 | "update": "Обновить",
33 | "upload": "Загрузить",
34 |
35 | "cut": "Вырезать",
36 | "copy": "Копировать",
37 | "paste": "Вставить"
38 | },
39 |
40 | "states": {
41 | "disabled": "Отключено",
42 | "enabled": "Включено",
43 | "loading": "Загрузка...",
44 | "connecting": "Соединение...",
45 | "saving": "Сохранение..."
46 | },
47 |
48 | "hotkeys": {
49 | "control": "Ctrl",
50 | "command": "Cmd",
51 | "shift": "Shift",
52 | "delete": "Del",
53 | "return": "Return"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/locales/pt-BR/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "Português do Brasil",
3 |
4 | "namePatternDescription": "Os seguintes caracteres não podem ser utilizados: \\, /, :, *, ?, \", <, >, |, [ e ].",
5 | "none": "(Nenhum)",
6 |
7 | "findAsset": {
8 | "placeholder": "Buscar por assets"
9 | },
10 |
11 | "actions": {
12 | "applyChanges": "Aplicar alterações",
13 | "applyChangesWithErrors": "Aplicar alterações com erros",
14 | "cancel": "Cancelar",
15 | "clear": "Limpar",
16 | "close": "Fechar",
17 | "create": "Criar",
18 | "delete": "Excluir",
19 | "download": "Baixar",
20 | "duplicate": "Duplicar",
21 | "export": "Exportar",
22 | "filter": "Filtrar",
23 | "import": "Importar",
24 | "new": "Novo",
25 | "open": "Abrir",
26 | "rename": "Renomear",
27 | "restore": "Restaurar",
28 | "save": "Salvar",
29 | "search": "Pesquisar",
30 | "select": "Selecionar",
31 | "skip": "Pular",
32 | "update": "Atualizar",
33 | "upload": "Enviar",
34 |
35 | "cut": "Cortar",
36 | "copy": "Copiar",
37 | "paste": "Colar"
38 | },
39 |
40 | "states": {
41 | "disabled": "Desabilitado",
42 | "enabled": "Habilitado",
43 | "loading": "Carregando...",
44 | "connecting": "Conectando...",
45 | "saving": "Salvando..."
46 | },
47 |
48 | "hotkeys": {
49 | "control": "Ctrl",
50 | "command": "Cmd",
51 | "shift": "Shift",
52 | "delete": "Del",
53 | "return": "Enter"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client/hub/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="utf-8")
5 | title #{t("hub:title")} — Superpowers
6 | link(rel="stylesheet",href="/styles/reset.css")
7 | link(rel="stylesheet",href="/styles/resizeHandle.css")
8 | link(rel="stylesheet",href="/styles/treeView.css")
9 | link(rel="stylesheet",href="/styles/dialogs.css")
10 | link(rel="stylesheet",href="/hub/index.css")
11 |
12 | body
13 | .server-header
14 | img.server-icon(src="/images/icon.png")
15 | .server-name= t("common:states.connecting")
16 |
17 | .projects-header
18 | .projects-buttons.button-strip
19 | button(disabled).new-project= t("hub:newProject.title")
20 | button(disabled).open-project= t("hub:openProject")
21 | button(disabled).edit-project= t("hub:editDetails.title")
22 |
23 | .language-container
24 | span= t("hub:language")
25 | select.language
26 |
27 | .projects-tree-view
28 | .tree-loading
29 | div= t("common:states.connecting")
30 |
31 | a(href="http://superpowers-html5.com/",target="_blank").server-footer
32 | | Superpowers — HTML5 2D+3D game maker
33 |
34 | script.
35 | if (window.navigator.userAgent.indexOf("Electron") !== -1) {
36 | document.body.querySelector(".server-header") .hidden = true;
37 | document.body.querySelector(".server-footer") .hidden = true;
38 | }
39 | script(src="/SupCore.js")
40 | script(src="/SupClient.js")
41 | script(src="/hub/index.js")
42 |
--------------------------------------------------------------------------------
/public/locales/es/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "Ir al hub",
4 | "publish": "Publicar proyecto",
5 | "publishDisabled": "Publicar proyecto (solamente funciona desde la app de Superpowers por razones técnicas)",
6 | "stop": "Parar proyecto",
7 | "debug": "Depurar proyecto (F6)",
8 | "run": "Ejecutar proyecto (F5)",
9 | "notifications": {
10 | "enable": "Haz click para habilitar las notificaciones",
11 | "disable": "Haz click para deshabilitar las notificaciones",
12 | "new": "Nuevo mensaje de chat en el proyecto \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Abrir en ventana nueva",
17 | "newAsset": {
18 | "title": "Nuevo asset",
19 | "prompt": "Selecciona el tipo y escribe un nombre para el nuevo asset.",
20 | "placeholder": "Nombre del asset (opcional)",
21 | "openAfterCreation": "Abrir después de crear"
22 | },
23 | "newFolder": {
24 | "title": "Nueva carpeta",
25 | "prompt": "Escribe un nombre para la nueva carpeta.",
26 | "placeholder": "Escribe un nombre",
27 | "initialValue": "Carpeta"
28 | },
29 | "renamePrompt": "Escribe un nombre para el asset.",
30 | "duplicatePrompt": "Escribe un nombre para el nuevo asset.",
31 | "trash": {
32 | "title": "Borrar",
33 | "prompt": "¿Seguro que quieres borrar los elementos seleccionados?",
34 | "warnBrokenDependency": "${entryName} está siendo usado en ${dependentEntryNames}."
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/SupClient/styles/properties.styl:
--------------------------------------------------------------------------------
1 | table.properties
2 | width 100%
3 | border-collapse collapse
4 | font-size 12px
5 |
6 | th, td { border: 1px solid #ccc; }
7 |
8 | th
9 | text-align left
10 | white-space nowrap
11 | overflow-x hidden
12 | text-overflow ellipsis
13 | font-weight normal
14 | background #eee
15 | padding 0 0.5em
16 | th > div
17 | display flex
18 | align-items center
19 | th > div > div
20 | flex 1
21 | white-space nowrap
22 | overflow-x hidden
23 | text-overflow ellipsis
24 | max-width 100px
25 | th:only-child
26 | background #777
27 | color #eee
28 | padding 0.5em
29 |
30 | td input, td select, td textarea
31 | width 100%
32 |
33 | td input, td select, td textarea
34 | margin 0
35 | padding 0.5em 0.25em
36 | border none
37 |
38 | td select { padding: 0.25em 0; }
39 |
40 | td input[type=checkbox]
41 | width auto
42 | margin 0.5em
43 | cursor pointer
44 |
45 | td input.color
46 | font-family "Consolas", monospace
47 |
48 | td input[type=color]
49 | padding 0
50 |
51 | td input[readonly]
52 | color #888
53 |
54 | td .inputs
55 | display flex
56 | align-items center
57 | > input:not([type=checkbox]) { flex: 1; }
58 | > input:not(:last-of-type) { border-right: 1px solid #ccc; }
59 | > select { flex: 1; }
60 | > select:not(:last-of-type) { border-right: 1px solid #ccc; }
61 |
62 | td .list
63 | input:not(:last-of-type) { border-bottom: 1px solid #ccc; }
64 |
--------------------------------------------------------------------------------
/public/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "activeLanguage": "English",
3 |
4 | "namePatternDescription": "The following characters cannot be used: \\, /, :, *, ?, \", <, >, |, [ and ].",
5 | "none": "(None)",
6 |
7 | "findAsset": {
8 | "placeholder": "Search for assets",
9 | "moreResults": "and ${results} more results...",
10 | "noResults": "No results found."
11 | },
12 |
13 | "actions": {
14 | "applyChanges": "Apply changes",
15 | "applyChangesWithErrors": "Apply changes with errors",
16 | "cancel": "Cancel",
17 | "clear": "Clear",
18 | "close": "Close",
19 | "create": "Create",
20 | "delete": "Delete",
21 | "download": "Download",
22 | "duplicate": "Duplicate",
23 | "export": "Export",
24 | "filter": "Filter",
25 | "import": "Import",
26 | "new": "New",
27 | "open": "Open",
28 | "rename": "Rename",
29 | "restore": "Restore",
30 | "save": "Save",
31 | "search": "Search",
32 | "select": "Select",
33 | "skip": "Skip",
34 | "update": "Update",
35 | "upload": "Upload",
36 |
37 | "cut": "Cut",
38 | "copy": "Copy",
39 | "paste": "Paste"
40 | },
41 |
42 | "states": {
43 | "disabled": "Disabled",
44 | "enabled": "Enabled",
45 | "loading": "Loading...",
46 | "connecting": "Connecting...",
47 | "saving": "Saving..."
48 | },
49 |
50 | "hotkeys": {
51 | "control": "Ctrl",
52 | "command": "Cmd",
53 | "shift": "Shift",
54 | "delete": "Del",
55 | "return": "Return"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SupCore/Data/Rooms.ts:
--------------------------------------------------------------------------------
1 | import * as SupData from "./index";
2 | import * as path from "path";
3 |
4 | const roomRegex = /^[A-Za-z0-9_]{1,20}$/;
5 |
6 | export default class Rooms extends SupData.Base.Dictionary {
7 | constructor(public server: ProjectServer) {
8 | super();
9 | }
10 |
11 | acquire(id: string, owner: any, callback: (err: Error, item?: any) => any) {
12 | if (!roomRegex.test(id)) { callback( new Error(`Invalid room id: ${id}`)); return; }
13 |
14 | super.acquire(id, owner, (err: Error, item: SupData.Room) => {
15 | if (err != null) { callback(err); return; }
16 | if (owner == null) { callback(null, item); return; }
17 |
18 | item.join(owner, (err: string, roomUser: any, index: number) => {
19 | if (err != null) { callback(new Error(err)); return; }
20 | this.server.io.in(`sub:rooms:${id}`).emit("edit:rooms", id, "join", roomUser, index);
21 | callback(null, item);
22 | });
23 | });
24 | }
25 |
26 | release(id: string, owner: any, options?: any) {
27 | super.release(id, owner, options);
28 | if (owner == null) return;
29 |
30 | this.byId[id].leave(owner, (err: string, roomUserId: string) => {
31 | if (err != null) throw new Error(err);
32 | this.server.io.in(`sub:rooms:${id}`).emit("edit:rooms", id, "leave", roomUserId);
33 | });
34 | }
35 |
36 | _load(id: string) {
37 | const room = new SupData.Room(null);
38 |
39 | room.load(path.join(this.server.projectPath, `rooms/${id}`));
40 |
41 | return room;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/SupClient/styles/dialogs.styl:
--------------------------------------------------------------------------------
1 | .dialog {
2 | width: 100%;
3 | height: 100%;
4 | z-index: 10;
5 | position: absolute;
6 | top: 0;
7 | left: 0;
8 | background-color: rgba(128,128,128,0.5);
9 | display: flex;
10 |
11 | form {
12 | background-color: white;
13 | margin: auto;
14 | padding: 10px;
15 | width: 450px;
16 | display: flex;
17 | flex-flow: column;
18 | box-shadow: 0 0 20px rgba(0,0,0,0.2);
19 | }
20 |
21 | header {
22 | text-transform: uppercase;
23 | font-size: 1.5em;
24 | margin-bottom: 0.5em;
25 | }
26 |
27 | .group { margin-bottom: 0.5em; }
28 | .checkbox.group { display: flex; align-items: center; padding: 0.5em; background: #fafafa; }
29 |
30 | input, select, textarea { padding: 0.125em 0.25em; }
31 |
32 | .buttons {
33 | display: flex;
34 | justify-content: flex-end;
35 |
36 | > button, > .button-strip { margin-left: 5px; }
37 | }
38 | }
39 |
40 | .find-asset-dialog {
41 | form { width: 600px; }
42 |
43 | input[type=search] { height: 28px; }
44 |
45 | .filter-container {
46 | flex-wrap: wrap;
47 |
48 | img { padding: 2px; width: 28px; height: 28px; }
49 | img.toggle-all { content: url(/images/entries/filter-all.svg); }
50 | img:hover { background: #ccc; }
51 | img.filtered { opacity: 0.3; }
52 | img.filtered:hover { opacity: 0.5; }
53 | }
54 |
55 | .assets-tree-view {
56 | border: 1px solid #aaa;
57 | overflow-y: auto;
58 | height: 300px;
59 | }
60 |
61 | .assets-tree-view .tree { padding: 0; }
62 | }
63 |
--------------------------------------------------------------------------------
/public/locales/de/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "Zum Hub",
4 | "publish": "Projekt veröffentlichen",
5 | "publishDisabled": "Projekt veröffentlichen (funktioniert aus technischen Gründen nur aus der Superpowers App)",
6 | "stop": "Projekt stoppen",
7 | "debug": "Projekt debuggen (F6)",
8 | "run": "Projekt starten (F5)",
9 | "notifications": {
10 | "enable": "Klicke um Benachrichtigungen zu aktivieren",
11 | "disable": "Klicke um Benachrichtigungen zu deaktivieren",
12 | "new": "Neue Chatnachricht beim Projekt \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Öffne in neuem Fenster",
17 | "newAsset": {
18 | "title": "Neues Asset",
19 | "prompt": "Wähle einen Typ und trage einen Namen für das neue Asset ein.",
20 | "placeholder": "Asset Name (optional)",
21 | "openAfterCreation": "Öffne nach Erstellung"
22 | },
23 | "newFolder": {
24 | "title": "Neues Verzeichnis",
25 | "prompt": "Trage einen Namen für das neue Verzeichnis ein.",
26 | "placeholder": "Trage einen Namen ein",
27 | "initialValue": "Verzeichnis"
28 | },
29 | "renamePrompt": "Trage einen Namen für das Asset ein.",
30 | "duplicatePrompt": "Trage einen Namen für das neue Asset ein.",
31 | "trash": {
32 | "title": "Papierkorb",
33 | "prompt": "Bist du sicher das du die ausgewählten Einträge löschen willst?",
34 | "warnBrokenDependency": "${entryName} wird verwendet in ${dependentEntryNames}."
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/client/login/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="utf-8")
5 | title #{t("login:title")} — Superpowers
6 | link(rel="stylesheet",href="/styles/reset.css")
7 | link(rel="stylesheet",href="/styles/properties.css")
8 | link(rel="stylesheet",href="/login/index.css")
9 |
10 | body
11 | .server-header
12 | img.server-icon(src="/images/icon.png")
13 | .server-name= t("common:states.connecting")
14 | .connecting
15 | p= t("common:states.connecting")
16 | form.login(method="post",hidden)
17 | .welcome= t("login:welcome")
18 |
19 | table.properties
20 | tr
21 | th= t("login:username")
22 | td
23 | // NOTE: The pattern and min/max lengths must match the regex in server/passportMiddleware.ts
24 | input.username(type="text",name="username",placeholder="Your username",spellcheck="false",minlength="3",maxlength="20",pattern="[A-Za-z0-9_-]+",title=t("login:usernamePatternDescription"),autofocus)
25 |
26 | .buttons
27 | button.log-in= t("login:logIn")
28 |
29 | a(href="http://superpowers-html5.com/",target="_blank").server-footer
30 | | Superpowers — HTML5 2D+3D game maker
31 |
32 | script.
33 | if (window.navigator.userAgent.indexOf("Electron") !== -1) {
34 | document.body.querySelector(".server-header") .hidden = true;
35 | document.body.querySelector(".server-footer") .hidden = true;
36 | }
37 | script(src="/SupCore.js")
38 | script(src="/SupClient.js")
39 | script(src="/login/index.js")
40 |
--------------------------------------------------------------------------------
/public/locales/fr/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "goToHub": "Aller à l'accueil du serveur",
4 | "publish": "Publier le projet",
5 | "publishDisabled": "Publier le projet (uniquement disponible dans l'application Superpowers pour des raisons techniques)",
6 | "stop": "Arrêter le projet",
7 | "debug": "Déboguer le projet (F6)",
8 | "run": "Tester le projet (F5)",
9 | "notifications": {
10 | "enable": "Cliquez pour activer les notifications",
11 | "disable": "Cliquez pour désactiver les notifications",
12 | "new": "Nouveau message de chat dans le projet \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
17 | "newAsset": {
18 | "title": "Nouvel asset",
19 | "prompt": "Sélectionnez le type d'asset et son nom",
20 | "placeholder": "Nom de l'asset (optionnel)",
21 | "openAfterCreation": "Ouvrir après création"
22 | },
23 | "newFolder": {
24 | "title": "Nouveau dossier",
25 | "prompt": "Nommez le nouveau dossier.",
26 | "placeholder": "Nom du dossier",
27 | "initialValue": "Dossier"
28 | },
29 | "renamePrompt": "Entrez un nouveau nom pour l'asset.",
30 | "duplicatePrompt": "Nommez le nouvel asset ou dossier.",
31 | "trash": {
32 | "title": "Supprimer",
33 | "prompt": "Êtes-vous sûr de vouloir supprimer les éléments sélectionnés ?",
34 | "checkbox": "Je comprends que cette action ne peut pas être annulée.",
35 | "warnBrokenDependency": "${entryName} est utilisé dans ${dependentEntryNames}."
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/client/project/tabs/homeTab.ts:
--------------------------------------------------------------------------------
1 | // FIXME: This shouldn't be directly on the client since it comes from a plugin?
2 |
3 | import { manifest } from "../network";
4 | import * as tabs from "./";
5 |
6 | let homeTab: HTMLLIElement;
7 | export function setup(tab: HTMLLIElement) {
8 | homeTab = tab;
9 | }
10 |
11 | export function onMessageChat(message: string) {
12 | if (homeTab == null) return;
13 |
14 | const isHomeTabVisible = homeTab.classList.contains("active");
15 | if (isHomeTabVisible && !document.hidden) return;
16 |
17 | if (!isHomeTabVisible) homeTab.classList.add("unread");
18 |
19 | if (localStorage.getItem("superpowers-disable-notifications") != null) return;
20 |
21 | function doNotification() {
22 | const title = SupClient.i18n.t("project:header.notifications.new", { projectName: manifest.pub.name });
23 | const notification = new (window as any).Notification(title, { icon: "/images/icon.png", body: message });
24 |
25 | const closeTimeoutId = setTimeout(() => { notification.close(); }, 5000);
26 |
27 | notification.addEventListener("click", () => {
28 | window.focus();
29 | tabs.onActivate(homeTab);
30 | clearTimeout(closeTimeoutId);
31 | notification.close();
32 | });
33 | }
34 |
35 | if ((window as any).Notification.permission === "granted") doNotification();
36 | else if ((window as any).Notification.permission !== "denied") {
37 | (window as any).Notification.requestPermission((status: string) => {
38 | (window as any).Notification.permission = status;
39 | if ((window as any).Notification.permission === "granted") doNotification();
40 | });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/locales/ru/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Проект",
3 | "header": {
4 | "goToHub": "Перейти в хаб",
5 | "buildDisabled": "Собрать проект (по техническим причинам, работает только в программе Superpowers)",
6 | "stop": "Остановить проект",
7 | "debug": "Отладка проекта (F6)",
8 | "run": "Запустить проект (F5)",
9 | "notifications": {
10 | "enable": "Нажмите, чтобы включить уведомления",
11 | "disable": "Нажмите, чтобы отключить уведомления",
12 | "new": "Новое сообщение в чате проекта \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Открыть в новом окне",
17 | "newAsset": {
18 | "title": "Новый ассет",
19 | "prompt": "Выберите тип и введите имя для нового ассета.",
20 | "placeholder": "Имя ассета (не обязательно)",
21 | "openAfterCreation": "Открыть после создания"
22 | },
23 | "newFolder": {
24 | "title": "Новая папка",
25 | "prompt": "Введите имя для новой папки.",
26 | "placeholder": "Имя папки",
27 | "initialValue": "Папка"
28 | },
29 | "renamePrompt": "Введите новое имя для ассета.",
30 | "duplicatePrompt": "Введите имя для нового ассета или папки.",
31 | "trash": {
32 | "title": "Удаление",
33 | "prompt": "Вы уверенны, что хотите удалить выбранные элементы?",
34 | "warnBrokenDependency": "${entryName} используется в ${dependentEntryNames}."
35 | }
36 | },
37 | "build": {
38 | "title": "Сборка проекта",
39 | "build": "Собрать"
40 | },
41 | "revision": {
42 | "title": "Ревизия",
43 | "prompt": "Введите имя для новой версии",
44 | "current": "Текущая",
45 | "restoring": "Восстановление..."
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/locales/en/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Project",
3 | "header": {
4 | "goToHub": "Go to hub",
5 | "buildDisabled": "Build project (only works from the Superpowers app for technical reasons)",
6 | "stop": "Stop project",
7 | "debug": "Debug project (F6)",
8 | "run": "Run project (F5)",
9 | "notifications": {
10 | "enable": "Click to enable notifications",
11 | "disable": "Click to disable notifications",
12 | "new": "New chat message in \"${projectName}\" project"
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Open in new window",
17 | "newAsset": {
18 | "title": "New asset",
19 | "prompt": "Select the type and enter a name for the new asset.",
20 | "placeholder": "Asset name (optional)",
21 | "openAfterCreation": "Open after creation"
22 | },
23 | "newFolder": {
24 | "title": "New folder",
25 | "prompt": "Enter a name for the new folder.",
26 | "placeholder": "Folder name",
27 | "initialValue": "Folder"
28 | },
29 | "renamePrompt": "Enter a new name for the asset.",
30 | "duplicatePrompt": "Enter a name for the new asset or folder.",
31 | "trash": {
32 | "title": "Trash",
33 | "prompt": "Are you sure you want to trash the selected entries?",
34 | "checkbox": "I understand this cannot be undone.",
35 | "warnBrokenDependency": "${entryName} is used in ${dependentEntryNames}."
36 | }
37 | },
38 | "build": {
39 | "title": "Build project",
40 | "build": "Build"
41 | },
42 | "revision": {
43 | "title": "Revision",
44 | "prompt": "Enter a name for the new revision.",
45 | "current": "Current",
46 | "restoring": "Restoring..."
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/public/locales/pt-BR/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Projeto",
3 | "header": {
4 | "goToHub": "Ir para o hub",
5 | "buildDisabled": "Publicar projeto (funciona apenas quando executado diretamente no Superpowers, por razões técnicas)",
6 | "stop": "Parar projeto",
7 | "debug": "Depurar projeto (F6)",
8 | "run": "Executar projeto (F5)",
9 | "notifications": {
10 | "enable": "Clique para habilitar as notificações",
11 | "disable": "Clique para desabilitar as notificações",
12 | "new": "Nova mensagem no chat do projeto \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Abrir em uma nova janela",
17 | "newAsset": {
18 | "title": "Novo asset",
19 | "prompt": "Selecione um tipo e um nome para este novo asset.",
20 | "placeholder": "Nome do asset (opcional)",
21 | "openAfterCreation": "Abrir após a criação"
22 | },
23 | "newFolder": {
24 | "title": "Nova pasta",
25 | "prompt": "Insira um nome para esta nova pasta.",
26 | "placeholder": "Insira um nome",
27 | "initialValue": "Pasta"
28 | },
29 | "renamePrompt": "Insira um novo nome para o asset.",
30 | "duplicatePrompt": "Insira um nome para o novo asset ou pasta.",
31 | "trash": {
32 | "title": "Lixeira",
33 | "prompt": "Você certeza que deseja enviar os itens selecionados para a lixeira?",
34 | "warnBrokenDependency": "${entryName} está sendo usado por ${dependentEntryNames}."
35 | }
36 | },
37 | "build": {
38 | "title": "Publicar projeto",
39 | "build": "Publicar"
40 | },
41 | "revision": {
42 | "title": "Revisão",
43 | "prompt": "Insira um nome para a nova revisão.",
44 | "current": "Atual",
45 | "restoring": "Restaurando..."
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/locales/it/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Progetto",
3 | "header": {
4 | "goToHub": "Vai all'hub",
5 | "buildDisabled": "Compila il progetto (funziona solamente dall'app Superpowers per motivi tecnici)",
6 | "stop": "Ferma il progetto",
7 | "debug": "Debug progetto (F6)",
8 | "run": "Lancia il progetto (F5)",
9 | "notifications": {
10 | "enable": "Clicca per abilitare le notifiche",
11 | "disable": "Clicca per disabilitare le notifiche",
12 | "new": "Nuovo messaggio nella chat del progetto \"${projectName}\""
13 | }
14 | },
15 | "treeView": {
16 | "openInNewWindow": "Apri in una nuova finestra",
17 | "newAsset": {
18 | "title": "Nuovo asset",
19 | "prompt": "Seleziona il tipo ed inserisci un nome per il nuovo asset.",
20 | "placeholder": "Nome dell'asset (opzionale)",
21 | "openAfterCreation": "Apri dopo la creazione"
22 | },
23 | "newFolder": {
24 | "title": "Nuova cartella",
25 | "prompt": "Inserisci un nome per la nuova cartella.",
26 | "placeholder": "Nome della cartella",
27 | "initialValue": "Cartella"
28 | },
29 | "renamePrompt": "Inserisci un nuovo nome per l'asset.",
30 | "duplicatePrompt": "Inserisci un nome per il nuovo asset o cartella.",
31 | "trash": {
32 | "title": "Cestino",
33 | "prompt": "Sei sicuro di voler eliminare gli elementi selezionati?",
34 | "warnBrokenDependency": "${entryName} è utilizzato in ${dependentEntryNames}."
35 | }
36 | },
37 | "build": {
38 | "title": "Compila progetto",
39 | "build": "Compila"
40 | },
41 | "revision": {
42 | "title": "Revisione",
43 | "prompt": "Inserisci un nome per la nuova revisione.",
44 | "current": "Corrente",
45 | "restoring": "In ripristino..."
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/SupClient/styles/buttons.styl:
--------------------------------------------------------------------------------
1 | button, .radio-strip label {
2 | background: linear-gradient(#efefef, #e5e5e5);
3 | padding: 0.3em 0.6em;
4 | border: 1px solid #999;
5 | border-radius: 2px;
6 | box-shadow: 0 0 1px 1px #fff inset;
7 | overflow: hidden;
8 | color: #222;
9 | line-height: 1;
10 | }
11 |
12 | button:not(:disabled):hover, .radio-strip input[type=radio]:not(:disabled) + label:hover {
13 | border-color: #777;
14 | }
15 |
16 | button:active, .radio-strip input[type=radio] + label:active {
17 | background: linear-gradient(#dadada, #e5e5e5);
18 | border-color: #aaa;
19 | box-shadow: 0 0 1px 1px #ccc inset;
20 | }
21 |
22 | .radio-strip input[type=radio]:checked + label {
23 | background: linear-gradient(#dadada, #d5d5d5);
24 | border-color: #888;
25 | box-shadow: 0 0 1px 1px #ccc inset;
26 | }
27 |
28 | button:disabled, .radio-strip input[type=radio]:disabled + label {
29 | color: #aaa;
30 | }
31 |
32 | .button-strip button, .radio-strip label {
33 | &:not(:last-of-type) { border-right: 0; border-radius: 0; }
34 | &:first-of-type { border-radius: 2px 0 0 2px; }
35 | &:last-of-type { border-radius: 0 2px 2px 0; }
36 | }
37 |
38 | .flat-button-strip {
39 | display: flex;
40 | background: rgba(0,0,0,0.05);
41 | border-bottom: 1px solid #acacac;
42 | }
43 |
44 | .flat-button-strip button {
45 | flex: 1;
46 | height: 35px;
47 | border: none;
48 | box-shadow: none;
49 | background-color: transparent;
50 | background-position: center center;
51 | background-repeat: no-repeat;
52 |
53 | opacity: 0.5;
54 | cursor: pointer;
55 |
56 | &:hover { background-color: rgba(0,0,0,0.1); opacity: 1; }
57 | &:active { background-color: #fff; }
58 | &:disabled { opacity: 0.2; cursor: default; }
59 | }
60 |
61 | .radio-strip input[type=radio] {
62 | display: none;
63 | }
64 |
--------------------------------------------------------------------------------
/SupCore/Data/Assets.ts:
--------------------------------------------------------------------------------
1 | import * as SupData from "./index";
2 | import * as fs from "fs";
3 | import * as path from "path";
4 |
5 | export default class Assets extends SupData.Base.Dictionary {
6 |
7 | byId: { [id: string]: SupCore.Data.Base.Asset };
8 |
9 | constructor(public server: ProjectServer) {
10 | super();
11 | }
12 |
13 | acquire(id: string, owner: SupCore.RemoteClient, callback: (err: Error, item: SupCore.Data.Base.Asset) => void) {
14 | if (this.server.data.entries.byId[id] == null || this.server.data.entries.byId[id].type == null) { callback(new Error(`Invalid asset id: ${id}`), null); return; }
15 |
16 | super.acquire(id, owner, callback);
17 | }
18 |
19 | release(id: string, owner: SupCore.RemoteClient, options?: { skipUnloadDelay: boolean }) {
20 | if (owner != null) this.byId[id].onClientUnsubscribed(owner.id);
21 |
22 | super.release(id, owner, options);
23 | }
24 |
25 | _load(id: string) {
26 | const entry = this.server.data.entries.byId[id];
27 |
28 | const assetClass = this.server.system.data.assetClasses[entry.type];
29 | if (assetClass == null) throw new Error(`No data plugin for asset type "${entry.type}"`);
30 |
31 | const asset = new assetClass(id, null, this.server);
32 |
33 | // NOTE: The way assets are laid out on disk was changed in Superpowers 0.11
34 | const oldDirPath = path.join(this.server.projectPath, `assets/${id}`);
35 | fs.stat(oldDirPath, (err, stats) => {
36 | const dirPath = path.join(this.server.projectPath, `assets/${this.server.data.entries.getStoragePathFromId(id)}`);
37 |
38 | if (stats == null) asset.load(dirPath);
39 | else {
40 | fs.rename(oldDirPath, dirPath, (err) => {
41 | if (err != null) throw err;
42 | asset.load(dirPath);
43 | });
44 | }
45 | });
46 | return asset;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/client/hub/index.styl:
--------------------------------------------------------------------------------
1 | @import "../shared"
2 |
3 | body {
4 | background: #eee;
5 | display: flex;
6 | flex-flow: column;
7 | }
8 |
9 | .projects-header {
10 | margin: 0.5em;
11 | display: flex;
12 | justify-content: space-between;
13 | }
14 |
15 | .language-container > span { margin-right: 0.5em; }
16 |
17 | .projects-tree-view {
18 | flex: 1;
19 | margin: 0 0.5em;
20 | margin-bottom: 0.5em;
21 | border: 1px solid #aaa;
22 | background: #fff;
23 | overflow-y: auto;
24 | position: relative;
25 |
26 | .connecting {
27 | position: absolute;
28 | top: 0;
29 | bottom: 0;
30 | left: 0;
31 | right: 0;
32 | z-index: 1;
33 | display: flex;
34 | flex-flow: column;
35 | align-items: center;
36 | justify-content: center;
37 | text-align: center;
38 | text-transform: uppercase;
39 | opacity: 0.5;
40 | }
41 |
42 | ol.tree {
43 | position: absolute;
44 | font-size: 1.5em;
45 | line-height: 1.25;
46 | }
47 |
48 | ol.tree li.item span,
49 | ol.tree li.group span {
50 | padding: 0;
51 | }
52 |
53 | ol.tree li {
54 | display: flex;
55 | white-space: nowrap;
56 | padding: 0.5em;
57 | align-items: flex-start;
58 | height: auto;
59 | }
60 |
61 | img {
62 | width: 48px;
63 | height: 48px;
64 | margin-right: 0.5em;
65 | border: 1px solid rgba(0,0,0,0.2);
66 | border-radius: 4px;
67 | background: #eee;
68 | }
69 |
70 | .info {
71 | flex: 1;
72 |
73 | > div {
74 | overflow: hidden;
75 | text-overflow: ellipsis;
76 | }
77 | }
78 |
79 | .name {
80 | font-weight: bold;
81 | }
82 |
83 | .details {
84 | font-size: 0.7em;
85 | .description { color: rgba(0, 0, 0, 0.8); }
86 | .project-type { color: rgba(0, 0, 0, 0.5); visibility: hidden; }
87 | .description:not(:empty) + .project-type:before { content: " — "; }
88 | }
89 |
90 | ol.tree li:hover {
91 | .project-type { visibility: visible; }
92 | }
93 | }
94 |
95 | .dialog .template-description:not(:empty) + .system-description:not(:empty) {
96 | margin-top: 0.5em;
97 | }
98 |
--------------------------------------------------------------------------------
/public/images/tabs/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/server/schemas.ts:
--------------------------------------------------------------------------------
1 | import * as tv4 from "tv4";
2 |
3 | // Server config
4 | const config = {
5 | type: "object",
6 | properties: {
7 | serverName: { type: "string" },
8 | mainPort: { type: "number" },
9 | buildPort: { type: "number" },
10 | password: { type: "string" },
11 | sessionSecret: { type: "string" },
12 | maxRecentBuilds: { type: "number", min: 1 }
13 | }
14 | };
15 |
16 | // Project manifest
17 | const projectManifest = {
18 | type: "object",
19 | properties: {
20 | id: { type: "string", minLength: 4, maxLength: 4 },
21 | name: { type: "string", minLength: 1, maxLength: 80 },
22 | description: { type: "string", maxLength: 300 },
23 | // Introduced in Superpowers v0.15
24 | system: { type: "string" },
25 | formatVersion: { type: "integer", min: 0 }
26 | },
27 | required: [ "id", "name", "description" ]
28 | };
29 |
30 | // Project entries
31 | const projectEntry = {
32 | type: "object",
33 | properties: {
34 | // IDs used to be integers but are now serialized as strings
35 | id: { type: [ "integer", "string" ] },
36 | name: { type: "string", minLength: 1, maxLength: 80 },
37 | type: { type: [ "string", "null" ] },
38 | children: {
39 | type: "array",
40 | items: { $ref: "#/definitions/projectEntry" }
41 | }
42 | },
43 | required: [ "id", "name" ]
44 | };
45 |
46 | const projectEntries = {
47 | definitions: { projectEntry },
48 | type: "object",
49 | properties: {
50 | // IDs used to be integers but are now serialized as strings
51 | id: { type: "integer" },
52 | nodes: {
53 | type: "array",
54 | items: { $ref: "#/definitions/projectEntry" }
55 | }
56 | },
57 | required: [ "nextEntryId", "nodes" ]
58 | };
59 |
60 | const schemas: { [name: string]: any } = { config, projectManifest, projectEntries };
61 |
62 | function validate(obj: any, schemaName: string) {
63 | const schema = schemas[schemaName];
64 | const result = tv4.validateResult(obj, schema);
65 |
66 | if (!result.valid) {
67 | throw new Error(`${result.error.dataPath} (${result.error.schemaPath}): ${result.error.message}`);
68 | }
69 |
70 | return true;
71 | }
72 |
73 | export { validate };
74 |
--------------------------------------------------------------------------------
/SupCore/Data/Base/Asset.ts:
--------------------------------------------------------------------------------
1 | import Hash from "./Hash";
2 |
3 | import * as path from "path";
4 | import * as fs from "fs";
5 |
6 | export default class Asset extends Hash {
7 | constructor(public id: string, pub: any, schema: SupCore.Data.Schema, public server: ProjectServer) {
8 | super(pub, schema);
9 | this.setMaxListeners(Infinity);
10 | if (this.server == null) this.setup();
11 | }
12 |
13 | init(options: any, callback: Function) { this.setup(); callback(); }
14 |
15 | setup() { /* Override */ }
16 |
17 | restore() { /* Override */ }
18 |
19 | onClientUnsubscribed(clientId: string) { /* Override */ }
20 |
21 | destroy(callback: Function) { callback(); }
22 |
23 | load(assetPath: string) {
24 | fs.readFile(path.join(assetPath, "asset.json"), { encoding: "utf8" }, (err, json) => {
25 | if (err != null) throw err;
26 |
27 | const pub = JSON.parse(json);
28 | this._onLoaded(assetPath, pub);
29 | });
30 | }
31 |
32 | _onLoaded(assetPath: string, pub: any) {
33 | this.migrate(assetPath, pub, (hasMigrated) => {
34 | if (hasMigrated) {
35 | this.pub = pub;
36 | this.save(assetPath, (err) => {
37 | this.setup();
38 | this.emit("load");
39 | });
40 | } else {
41 | this.pub = pub;
42 | this.setup();
43 | this.emit("load");
44 | }
45 | });
46 | }
47 |
48 | unload() { this.removeAllListeners(); }
49 |
50 | migrate(assetPath: string, pub: any, callback: (hasMigrated: boolean) => void) { callback(false); }
51 |
52 | client_load() { /* Override */ }
53 | client_unload() { /* Override */ }
54 |
55 | save(assetPath: string, callback: (err: Error) => any) {
56 | const json = JSON.stringify(this.pub, null, 2);
57 | fs.writeFile(path.join(assetPath, "asset.json"), json, { encoding: "utf8" }, callback);
58 | }
59 |
60 | server_setProperty(client: SupCore.RemoteClient, path: string, value: number|string|boolean, callback: SupCore.Data.Base.SetPropertyCallback) {
61 | this.setProperty(path, value, (err, actualValue) => {
62 | if (err != null) { callback(err); return; }
63 |
64 | callback(null, null, path, actualValue);
65 | });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/SupClient/typings/SupApp.d.ts:
--------------------------------------------------------------------------------
1 | import * as Electron from "electron";
2 |
3 | type ChooseFolderCallback = (folder: string) => void;
4 | type ChooseFileCallback = (filename: string) => void;
5 |
6 | declare global {
7 | namespace SupApp {
8 | export function onMessage(messageType: string, callback: Function): void;
9 | export function sendMessage(windowId: number, message: string, args?: any[]): void;
10 |
11 | export function getCurrentWindow(): Electron.BrowserWindow;
12 | export function showMainWindow(): void;
13 |
14 | export function openWindow(url: string, options?: OpenWindowOptions): Electron.BrowserWindow;
15 | interface OpenWindowOptions {
16 | size?: { width: number; height: number; };
17 | minSize?: { width: number; height: number; };
18 | resizable?: boolean;
19 | }
20 |
21 | export function openLink(url: string): void;
22 | export function showItemInFolder(path: string): void;
23 |
24 | export function createMenu(): Electron.Menu;
25 | export function createMenuItem(options: Electron.MenuItemConstructorOptions): Electron.MenuItem;
26 |
27 | export namespace clipboard {
28 | export function copyFromDataURL(dataURL: string): void;
29 | }
30 |
31 | export function chooseFolder(callback: ChooseFolderCallback): void;
32 | export function chooseFile(access: "readWrite" | "execute", callback: ChooseFileCallback): void;
33 | export function tryFileAccess(filePath: string, access: "readWrite" | "execute", callback: (err: Error) => void): void;
34 |
35 | export function mkdirp(folderPath: string, callback: (err: Error) => void): void;
36 | export function mktmpdir(callback: (err: Error, path: string) => void): void;
37 | export function writeFile(filename: string, data: any, callback: (err: NodeJS.ErrnoException) => void): void;
38 | export function writeFile(filename: string, data: any, options: any, callback: (err: NodeJS.ErrnoException) => void): void;
39 | export function readDir(folderPath: string, callback: (err: NodeJS.ErrnoException, files: string[]) => void): void;
40 | export function spawnChildProcess(filename: string, args: string[], callback: (err: Error, childProcess?: any) => void): void;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import * as yargs from "yargs";
4 | import { dataPath } from "./commands/utils";
5 |
6 | // Command line interface
7 | const argv = yargs
8 | .usage("Usage: $0 [options]")
9 | .demand(1, "Enter a command")
10 | .describe("data-path", "Path to store/read data files from, including config and projects")
11 | .command("start", "Start the server", (yargs) => yargs.demand(1, 1, `The "start" command doesn't accept any arguments`))
12 | .command("registry", "List registry content", (yargs) => yargs.demand(1, 1, `The "registry" command doesn't accept any arguments`))
13 | .command("install", "Install a system or plugin", (yargs) => yargs.demand(2, 2, `The "install" command requires a single argument: "systemId" or "systemId:pluginAuthor/pluginName"`))
14 | .command("uninstall", "Uninstall a system or plugin", (yargs) => yargs.demand(2, 2, `The "uninstall" command requires a single argument: "systemId" or "systemId:pluginAuthor/pluginName"`))
15 | .command("update", "Update the server, a system or a plugin", (yargs) =>
16 | yargs.demand(2, 2, `The "update" command requires a single argument: server, "systemId" or "systemId:pluginAuthor/pluginName"`))
17 | .command("init", "Generate a skeleton for a new system or plugin", (yargs) => yargs.demand(2, 2, `The "init" command requires a single argument: "systemId" or "systemId:pluginAuthor/pluginName"`))
18 | .help("h").alias("h", "help")
19 | .argv;
20 |
21 | const command = argv._[0];
22 | const [ systemId, pluginFullName ] = argv._[1] != null ? argv._[1].split(":") : [ null, null ];
23 | switch (command) {
24 | /* tslint:disable */
25 | case "start": require("./commands/start").default(dataPath); break;
26 | case "registry": require("./commands/registry").default(); break;
27 | case "install": require("./commands/install").default(systemId, pluginFullName); break;
28 | case "uninstall": require("./commands/uninstall").default(systemId, pluginFullName); break;
29 | case "update": require("./commands/update").default(systemId, pluginFullName); break;
30 | case "init": require("./commands/init").default(systemId, pluginFullName); break;
31 | /* tslint:enable */
32 | default:
33 | yargs.showHelp();
34 | process.exit(1);
35 | break;
36 | }
37 |
--------------------------------------------------------------------------------
/SupClient/styles/resizeHandle.styl:
--------------------------------------------------------------------------------
1 | .resize-handle {
2 | background-color: #ccc;
3 | background-clip: content-box;
4 | z-index: 2;
5 |
6 | &:not(.disabled):hover { background-color: #b0b0b0; }
7 |
8 | &.left {
9 | border-right: 5px solid transparent;
10 | margin-right: -5px;
11 | width: 6px;
12 | cursor: ew-resize;
13 | }
14 |
15 | &.right {
16 | border-left: 5px solid transparent;
17 | margin-left: -5px;
18 | width: 6px;
19 | cursor: ew-resize;
20 | }
21 |
22 | &.top {
23 | border-bottom: 5px solid transparent;
24 | margin-bottom: -5px;
25 | height: 6px;
26 | cursor: ns-resize;
27 | }
28 |
29 | &.bottom {
30 | border-top: 5px solid transparent;
31 | margin-top: -5px;
32 | height: 6px;
33 | cursor: ns-resize;
34 | }
35 |
36 | &.disabled { cursor: default; }
37 | }
38 |
39 | html.handle-dragging {
40 | * {
41 | -webkit-user-select: none;
42 | -moz-user-select: none;
43 | user-select: none;
44 | }
45 | iframe { pointer-events: none; }
46 |
47 | &.vertical * { cursor: ew-resize; }
48 | &.horizontal * { cursor: ns-resize; }
49 | }
50 |
51 | .collapsable {
52 | display: flex;
53 | flex-flow: column;
54 |
55 | &.collapsed {
56 | min-height: 0 !important;
57 | height: auto !important;
58 | }
59 |
60 | .header {
61 | cursor: pointer;
62 | background-color: #444;
63 | color: #fff;
64 | display: flex;
65 | align-items: center;
66 |
67 | &.has-draft { background-color: #37d; }
68 | &.has-errors { background-color: #a44; }
69 |
70 | button.toggle {
71 | width: 30px;
72 | align-self: stretch;
73 | background: transparent;
74 | border: none;
75 | box-shadow: none;
76 | padding: 0;
77 | opacity: 0.5;
78 | font-size 20px
79 | font-weight: bold;
80 | cursor: pointer;
81 | color: #fff;
82 |
83 | &:hover {
84 | opacity: 1;
85 | background-color: rgba(0,0,0,0.1);
86 | }
87 |
88 | &:active {
89 | background-color: #fff;
90 | color: #444;
91 | }
92 |
93 | &:disabled {
94 | opacity: 0.2;
95 | cursor: default;
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/client/project/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="utf-8")
5 | title #{t("project:title")} — Superpowers
6 | link(rel="stylesheet",href="/styles/reset.css")
7 | link(rel="stylesheet",href="/styles/resizeHandle.css")
8 | link(rel="stylesheet",href="/styles/tabStrip.css")
9 | link(rel="stylesheet",href="/styles/treeView.css")
10 | link(rel="stylesheet",href="/styles/dialogs.css")
11 | link(rel="stylesheet",href="/styles/properties.css")
12 | link(rel="stylesheet",href="/project/index.css")
13 |
14 | body(hidden)
15 | .sidebar
16 | .project-management
17 | .project-icon
18 | button(title=t("project:header.goToHub")).go-to-hub
19 | .project-name= t("common:states.loading")
20 | .project-buttons(hidden)
21 | button(title=t("project:build.title"),disabled).build
22 | button(title=t("project:header.stop"),disabled,hidden).stop
23 | button(title=t("project:header.debug"),disabled,hidden).debug
24 | button(title=t("project:header.run"),disabled,hidden).run
25 |
26 | .entries-buttons.flat-button-strip
27 | button(title=t("project:treeView.newAsset.title"),disabled,data-hotkey="control+N").new-asset
28 | button(title=t("project:treeView.newFolder.title"),disabled,data-hotkey="control+shift+N").new-folder
29 | button(title=t("common:actions.rename"),disabled,data-hotkey="F2").rename-entry.single.edit
30 | button(title=t("common:actions.duplicate"),disabled,data-hotkey="control+D").duplicate-entry.single.edit
31 | button(title=t("project:treeView.trash.title"),disabled,data-hotkey="delete").trash-entry.edit
32 | button(title=t("common:actions.search"),disabled,data-hotkey="control+O").search
33 | button(title=t("common:actions.filter"),disabled,data-hotkey="control+I").filter
34 | .filter-buttons.flat-button-strip(hidden)
35 | .entries-tree-view
36 | .tree-loading
37 | div= t("common:states.connecting")
38 | .tools
39 | ul
40 | .resize-handle.left
41 | .main
42 | .top
43 | .tabs-bar
44 | .controls
45 | button.toggle-notifications
46 | .panes
47 |
48 | script(src="/SupCore.js")
49 | script(src="/SupClient.js")
50 | script(src="/project/index.js")
51 |
--------------------------------------------------------------------------------
/scripts/getBuildPaths.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const fs = require("fs");
5 |
6 | const shouldIgnoreFolder = (folderName) => folderName.indexOf(".") !== -1 || folderName === "node_modules" || folderName === "public";
7 | const builtInFolderAuthors = [ "default", "common", "extra" ];
8 |
9 | module.exports = (rootPath) => {
10 | var buildPaths = [
11 | rootPath,
12 | `${rootPath}/SupCore`, `${rootPath}/SupClient`,
13 | `${rootPath}/server`, `${rootPath}/client`
14 | ];
15 |
16 | // Systems and plugins
17 | const systemsPath = `${rootPath}/systems`;
18 |
19 | let systemFolders = [];
20 | try { systemFolders = fs.readdirSync(systemsPath); }
21 | catch (err) { /* Ignore */ }
22 |
23 | systemFolders.forEach((systemName) => {
24 | if (shouldIgnoreFolder(systemName)) return;
25 |
26 | const systemPath = `${systemsPath}/${systemName}`;
27 | buildPaths.push(systemPath);
28 |
29 | let isDevFolder = true;
30 | try { if (!fs.lstatSync(`${systemPath}/.git`).isDirectory()) isDevFolder = false; }
31 | catch (err) { isDevFolder = false; }
32 | if (!isDevFolder) return;
33 |
34 | fs.readdirSync(systemPath).forEach((systemFolder) => {
35 | if (shouldIgnoreFolder(systemFolder) || systemFolder === "plugins") return;
36 | buildPaths.push(`${systemPath}/${systemFolder}`);
37 | });
38 |
39 | const systemPluginsPath = `${systemPath}/plugins`;
40 | if (!fs.existsSync(systemPluginsPath)) return;
41 |
42 | fs.readdirSync(systemPluginsPath).forEach((pluginAuthor) => {
43 | if (shouldIgnoreFolder(pluginAuthor)) return;
44 |
45 | const pluginAuthorPath = `${systemPluginsPath}/${pluginAuthor}`;
46 | fs.readdirSync(pluginAuthorPath).forEach((pluginName) => {
47 | if (shouldIgnoreFolder(pluginName)) return;
48 |
49 | const pluginPath = `${pluginAuthorPath}/${pluginName}`;
50 |
51 | if (builtInFolderAuthors.indexOf(pluginAuthor) === -1) {
52 | let isDevFolder = true;
53 | try { if (!fs.lstatSync(`${pluginPath}/.git`).isDirectory()) isDevFolder = false; }
54 | catch (err) { isDevFolder = false; }
55 | if (!isDevFolder) return;
56 | }
57 |
58 | buildPaths.push(pluginPath);
59 | });
60 | });
61 | });
62 |
63 | return buildPaths;
64 | };
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Superpowers — The extensible, real-time collaborative IDE
2 |
3 | [](https://github.com/superpowers/superpowers-core/blob/master/LICENSE.txt)
4 | [](https://travis-ci.org/superpowers/superpowers-core)
5 | [](https://gitter.im/superpowers/dev)
6 | [](https://www.patreon.com/SparklinLabs)
7 | [](https://twitter.com/SuperpowersDev)
8 |
9 | [Website](http://superpowers-html5.com/) —
10 | [Download](https://sparklinlabs.itch.io/superpowers) —
11 | [Documentation](http://docs.superpowers-html5.com/)
12 | [Roadmap](http://docs.superpowers-html5.com/en/development/roadmap) —
13 | [How to contribute](http://docs.superpowers-html5.com/en/development/how-to-contribute) —
14 | [Build instructions](http://docs.superpowers-html5.com/en/development/building-superpowers)
15 |
16 | ### Powerful, extensible, collaborative game development.
17 |
18 | Superpowers is a [downloadable HTML5 app](http://superpowers-html5.com/). You can use it solo like a regular offline game maker,
19 | or setup a password and let friends join in on your project through their Web browser.
20 | It's great for working together over long periods of time, for jamming over a weekend,
21 | or just for helping each other out with debugging!
22 |
23 | #### ... not just for making games!
24 |
25 | Here's the cool thing: Superpowers itself is actually engine-agnostic.
26 | It's just a piece of software for collaborating on projects that can be extended with new systems and plugins!
27 |
28 | * [Superpowers Game](https://github.com/superpowers/superpowers-game) — Make 2D+3D games with TypeScript, powered by Three.js
29 | * [Superpowers Web](http://github.com/superpowers/superpowers-web) — Make static websites with Pug and Stylus
30 | * [Superpowers LÖVE](https://github.com/superpowers/superpowers-love2d) — Make LÖVE 2D games with Lua
31 |
32 | Learn [how to extend Superpowers yourself](http://docs.superpowers-html5.com/en/development/extending-superpowers).
33 | Use it to make games, websites, slideshows, blogs, movies, apps, mods... whatever you can come up with!
34 |
35 | 
36 |
--------------------------------------------------------------------------------
/SupCore/Data/ProjectManifest.ts:
--------------------------------------------------------------------------------
1 | import Hash from "./Base/Hash";
2 |
3 | export default class ProjectManifest extends Hash {
4 | static schema: SupCore.Data.Schema = {
5 | id: { type: "string" },
6 | name: { type: "string", minLength: 1, maxLength: 80, mutable: true },
7 | description: { type: "string", maxLength: 300, mutable: true },
8 | systemId: { type: "string" },
9 | formatVersion: { type: "integer" }
10 | };
11 |
12 | static currentFormatVersion = 6;
13 | migratedFromFormatVersion: number;
14 |
15 | constructor(pub: SupCore.Data.ProjectManifestPub) {
16 | const migratedFromFormatVersion = ProjectManifest.migrate(pub);
17 | super(pub, ProjectManifest.schema);
18 | this.migratedFromFormatVersion = migratedFromFormatVersion;
19 | }
20 |
21 | static migrate(pub: SupCore.Data.ProjectManifestPub): number {
22 | if (pub.formatVersion === ProjectManifest.currentFormatVersion) return null;
23 | if (pub.formatVersion == null) pub.formatVersion = 0;
24 |
25 | if (pub.formatVersion > ProjectManifest.currentFormatVersion) {
26 | throw new Error("This project was created using a more recent version of Superpowers and cannot be loaded. " +
27 | `Format version is ${pub.formatVersion} but this version of Superpowers only supports up to ${ProjectManifest.currentFormatVersion}.`);
28 | }
29 |
30 | const oldFormatVersion = pub.formatVersion;
31 |
32 | if (oldFormatVersion === 0) {
33 | // Nothing to migrate here, the manifest itself didn't change
34 | // The on-disk project format did though, and will be updated
35 | // by ProjectServer based on oldFormatVersion
36 | }
37 |
38 | if (oldFormatVersion <= 1) {
39 | pub.systemId = "game";
40 | } else if (oldFormatVersion <= 3) {
41 | pub.systemId = (pub as any).system;
42 | delete (pub as any).system;
43 |
44 | switch (pub.systemId) {
45 | case "supGame":
46 | pub.systemId = "game";
47 | break;
48 | case "supWeb":
49 | pub.systemId = "web";
50 | break;
51 | case "markSlide":
52 | pub.systemId = "markslide";
53 | break;
54 | }
55 | } else if (oldFormatVersion <= 4) {
56 | pub.systemId = (pub as any).system;
57 | delete (pub as any).system;
58 | }
59 |
60 | pub.formatVersion = ProjectManifest.currentFormatVersion;
61 | return oldFormatVersion;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/client/build/index.ts:
--------------------------------------------------------------------------------
1 | import * as async from "async";
2 | import { BuildSetup } from "../project/sidebar/StartBuildDialog";
3 |
4 | document.addEventListener("keydown", (event) => {
5 | // F12
6 | if (event.keyCode === 123) SupApp.getCurrentWindow().webContents.toggleDevTools();
7 | });
8 |
9 | let socket: SocketIOClient.Socket;
10 |
11 | SupApp.onMessage("build", (buildSetup: BuildSetup, projectWindowId: number) => {
12 | socket = SupClient.connect(SupClient.query.project);
13 | socket.on("welcome", (clientId: number, config: { buildPort: number; supportsServerBuild: boolean; }) => {
14 | loadPlugins(buildSetup, () => {
15 | const buildPlugin = SupClient.getPlugins("build")[buildSetup.buildPluginName];
16 | buildPlugin.content.build(socket, buildSetup.settings, projectWindowId, config.buildPort);
17 | });
18 | });
19 | });
20 |
21 | const detailsContainer = document.querySelector(".details") as HTMLDivElement;
22 | const toggleDetailsButton = document.querySelector("button.toggle-details") as HTMLButtonElement;
23 | toggleDetailsButton.addEventListener("click", () => {
24 | detailsContainer.hidden = !detailsContainer.hidden;
25 | toggleDetailsButton.textContent = SupClient.i18n.t("build:" + (detailsContainer.hidden ? "showDetails" : "hideDetails"));
26 | SupApp.getCurrentWindow().setContentSize(SupApp.getCurrentWindow().getContentSize()[0], detailsContainer.hidden ? 150 : 350);
27 | });
28 |
29 | function loadPlugins(buildSetup: BuildSetup, callback: Function) {
30 | const i18nFiles: SupClient.i18n.File[] = [];
31 | i18nFiles.push({ root: "/", name: "build" });
32 | i18nFiles.push({ root: buildSetup.pluginPath, name: "builds" });
33 |
34 | SupClient.fetch(`/systems/${SupCore.system.id}/plugins.json`, "json", (err: Error, pluginsInfo: SupCore.PluginsInfo) => {
35 | SupCore.system.pluginsInfo = pluginsInfo;
36 |
37 | async.parallel([
38 | (cb) => {
39 | SupClient.i18n.load(i18nFiles, cb);
40 | }, (cb) => {
41 | async.each(pluginsInfo.list, (pluginName, cb) => {
42 | const pluginPath = `/systems/${SupCore.system.id}/plugins/${pluginName}`;
43 | SupClient.loadScript(`${pluginPath}/bundles/build.js`, cb);
44 | }, cb);
45 | }
46 | ], () => {
47 | document.querySelector("header").textContent = SupClient.i18n.t(`builds:${buildSetup.buildPluginName}.title`);
48 | callback();
49 | });
50 | });
51 | }
52 |
--------------------------------------------------------------------------------
/scripts/i18n.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const yargs = require("yargs");
4 |
5 | const fs = require("fs");
6 | exports.rootLocalesPath = `${__dirname}/../public/locales`;
7 | exports.relativeLocalesPath = "./public/locales";
8 |
9 | let fallbackLocale = null;
10 |
11 | exports.loadLocale = (languageCode, relative) => {
12 | if (languageCode === "none") return {};
13 | if (fallbackLocale == null && languageCode !== "en") fallbackLocale = exports.loadLocale("en", relative);
14 |
15 | const localePath = relative ? exports.relativeLocalesPath : exports.rootLocalesPath;
16 |
17 | const namespaces = {};
18 | if (relative) namespaces["common"] = JSON.parse(fs.readFileSync(`${exports.rootLocalesPath}/${languageCode}/common.json`, { encoding: "utf8" }));
19 |
20 | let filenames = [];
21 | try { filenames = fs.readdirSync(`${localePath}/${languageCode}`); } catch (err) { /* Ignore */ }
22 | for (const filename of filenames) {
23 | const file = fs.readFileSync(`${localePath}/${languageCode}/${filename}`, { encoding: "utf8" });
24 | namespaces[filename.slice(0, filename.lastIndexOf("."))] = JSON.parse(file);
25 | }
26 |
27 | if (languageCode !== "en") reportMissingKeys(languageCode, namespaces);
28 | return namespaces;
29 | };
30 |
31 | exports.makeT = (locale) => {
32 | return function t(path) {
33 | const parts = path.split(":");
34 | let value = locale[parts[0]];
35 | if (value == null) return path;
36 |
37 | const keys = parts[1].split(".");
38 | for (const key of keys) {
39 | value = value[key];
40 | if (value == null) return path;
41 | }
42 | return value;
43 | }
44 | };
45 |
46 | function reportMissingKeys(languageCode, locale) {
47 | const missingKeys = [];
48 |
49 | function checkRecursively(fallbackRoot, root, key, path) {
50 | if (root[key] == null) {
51 | missingKeys.push(path);
52 | root[key] = fallbackRoot[key];
53 | } else if (typeof fallbackRoot[key] === "object") {
54 | const childKeys = Object.keys(fallbackRoot[key]);
55 | for (const childKey of childKeys) checkRecursively(fallbackRoot[key], root[key], childKey, `${path}.${childKey}`);
56 | }
57 | }
58 |
59 | const rootKeys = Object.keys(fallbackLocale);
60 | for (const rootKey of rootKeys) checkRecursively(fallbackLocale, locale, rootKey, rootKey);
61 |
62 | if (missingKeys.length > 0 && yargs.silent) {
63 | console.log(`Missing keys in ${languageCode} locale: ${missingKeys.join(", ")}`);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/gulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const gulp = require("gulp");
4 |
5 | // Pug
6 | const pugTasks = [];
7 |
8 | const pug = require("gulp-pug");
9 | const rename = require("gulp-rename");
10 | const fs = require("fs");
11 |
12 | const i18n = require("../scripts/i18n.js");
13 | const languageCodes = fs.readdirSync(i18n.rootLocalesPath);
14 | languageCodes.push("none");
15 |
16 | for (const languageCode of languageCodes) {
17 | const locale = i18n.loadLocale(languageCode);
18 | gulp.task(`pug-${languageCode}`, () => {
19 | let result = gulp.src("./**/index.pug").pipe(pug({ locals: { t: i18n.makeT(locale) } }));
20 | if (languageCode !== "en") result = result.pipe(rename({ extname: `.${languageCode}.html` }));
21 | return result.pipe(gulp.dest("../public"));
22 | });
23 | pugTasks.push(`pug-${languageCode}`);
24 | }
25 |
26 | // Stylus
27 | const stylus = require("gulp-stylus");
28 | gulp.task("stylus", () => gulp.src("./**/index.styl").pipe(stylus({ errors: true, compress: true })).pipe(gulp.dest("../public")));
29 |
30 | // TypeScript
31 | const ts = require("gulp-typescript");
32 | const tsProject = ts.createProject("./tsconfig.json");
33 | const tslint = require("gulp-tslint");
34 |
35 | gulp.task("typescript", () => {
36 | const tsResult = tsProject.src()
37 | .pipe(tslint({ formatter: "prose" }))
38 | .pipe(tslint.report({ emitError: true }))
39 | .on("error", (err) => { throw err; })
40 | .pipe(tsProject())
41 | return tsResult.js.pipe(gulp.dest("./"));
42 | });
43 |
44 | // Browserify
45 | const browserify = require("browserify");
46 | const source = require("vinyl-source-stream");
47 |
48 | gulp.task("browserify-login", gulp.series("typescript", () => browserify("./login/index.js").bundle().pipe(source("index.js")).pipe(gulp.dest("../public/login"))));
49 | gulp.task("browserify-hub", gulp.series("typescript", () => browserify("./hub/index.js").bundle().pipe(source("index.js")).pipe(gulp.dest("../public/hub"))));
50 | gulp.task("browserify-project", gulp.series("typescript", () => browserify("./project/index.js").bundle().pipe(source("index.js")).pipe(gulp.dest("../public/project"))));
51 | gulp.task("browserify-build", gulp.series("typescript", () => browserify("./build/index.js").bundle().pipe(source("index.js")).pipe(gulp.dest("../public/build"))));
52 |
53 | // All
54 | gulp.task("default", gulp.parallel(
55 | gulp.parallel(pugTasks),
56 | "stylus",
57 | gulp.series("typescript", "browserify-login", "browserify-hub", "browserify-project", "browserify-build")));
58 |
--------------------------------------------------------------------------------
/SupCore/Data/Base/Resource.ts:
--------------------------------------------------------------------------------
1 | import Hash from "./Hash";
2 |
3 | import * as path from "path";
4 | import * as fs from "fs";
5 |
6 | export default class Resource extends Hash {
7 | constructor(public id: string, pub: any, schema: SupCore.Data.Schema, public server: ProjectServer) {
8 | super(pub, schema);
9 | if (server == null) this.setup();
10 | }
11 |
12 | init(callback: Function) { this.setup(); callback(); }
13 |
14 | setup() { /* Override */ }
15 |
16 | restore() { /* Override */ }
17 |
18 | load(resourcePath: string) {
19 | fs.readFile(path.join(resourcePath, "resource.json"), { encoding: "utf8" }, (err, json) => {
20 | if (err != null) {
21 | if (err.code === "ENOENT") {
22 | this.init(() => { this._onLoaded(resourcePath, this.pub, true); });
23 | return;
24 | }
25 | throw err;
26 | }
27 |
28 | const pub = JSON.parse(json);
29 | this._onLoaded(resourcePath, pub, false);
30 | });
31 | }
32 |
33 | _onLoaded(resourcePath: string, pub: any, justCreated: boolean) {
34 | if (justCreated) {
35 | this.pub = pub;
36 | fs.mkdir(path.join(resourcePath), (err) => {
37 | this.save(resourcePath, (err) => {
38 | this.setup();
39 | this.emit("load");
40 | });
41 | });
42 | return;
43 | }
44 |
45 | this.migrate(resourcePath, pub, (hasMigrated) => {
46 | if (hasMigrated) {
47 | this.pub = pub;
48 | fs.mkdir(path.join(resourcePath), (err) => {
49 | this.save(resourcePath, (err) => {
50 | this.setup();
51 | this.emit("load");
52 | });
53 | });
54 | } else {
55 | this.pub = pub;
56 | this.setup();
57 | this.emit("load");
58 | }
59 | });
60 | }
61 |
62 | unload() { this.removeAllListeners(); }
63 |
64 | migrate(resourcePath: string, pub: any, callback: (hasMigrated: boolean) => void) { callback(false); }
65 |
66 | save(resourcePath: string, callback: (err: Error) => any) {
67 | const json = JSON.stringify(this.pub, null, 2);
68 | fs.writeFile(path.join(resourcePath, "resource.json"), json, { encoding: "utf8" }, callback);
69 | }
70 |
71 | server_setProperty(client: SupCore.RemoteClient, path: string, value: number|string|boolean, callback: SupCore.Data.Base.SetPropertyCallback) {
72 | this.setProperty(path, value, (err, actualValue) => {
73 | if (err != null) { callback(err); return; }
74 |
75 | callback(null, null, path, actualValue);
76 | });
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/SupClient/styles/treeView.styl:
--------------------------------------------------------------------------------
1 | ol.tree {
2 | list-style: none;
3 | line-height: 1.5;
4 | margin: 0;
5 | padding: 0 0 2em 0;
6 | width: 100%;
7 | min-height: 100%;
8 |
9 | * {
10 | -webkit-user-select: none;
11 | -moz-user-select: none;
12 | user-select: none;
13 | }
14 |
15 | &.drop-inside:before {
16 | position: absolute;
17 | content: "";
18 | border-top: 1px solid #888;
19 | left: 0;
20 | right: 0;
21 | }
22 |
23 | ol {
24 | list-style: none;
25 | margin: 0;
26 | padding-left: 24px;
27 |
28 | &:last-of-type.drop-below {
29 | border-bottom: 1px solid #888;
30 | padding-bottom: 0;
31 | }
32 | }
33 |
34 | li.item, li.group {
35 | background-clip: border-box;
36 | height: 28px;
37 | display: flex;
38 | padding: 1px;
39 | cursor: default;
40 | display: flex;
41 | align-items: center;
42 |
43 | > .icon, > .toggle {
44 | margin: -1px;
45 | width: 24px;
46 | height: 24px;
47 | }
48 |
49 | span {
50 | align-self: center;
51 | padding: 0.25em;
52 | }
53 |
54 | &:hover { background-color: #eee; }
55 |
56 | &.drop-above {
57 | border-top: 1px solid #888;
58 | padding-top: 0;
59 | }
60 |
61 | &.drop-inside {
62 | border: 1px solid #888;
63 | padding: 0;
64 | }
65 |
66 | &.selected { background: #beddf4; }
67 | }
68 |
69 | li.item.drop-below {
70 | border-bottom: 1px solid #888;
71 | padding-bottom: 0;
72 | }
73 |
74 | li.group {
75 | color: #444;
76 |
77 | > .toggle {
78 | opacity: 0.5;
79 | background: url(/images/treeView/group-open-dark.svg) center no-repeat;
80 | cursor: pointer;
81 | }
82 |
83 | &.drop-below {
84 | + ol {
85 | border-bottom: 1px solid #888;
86 |
87 | &:empty {
88 | margin-top: -1px;
89 | pointer-events: none;
90 | }
91 | }
92 | }
93 | }
94 |
95 | li.group.collapsed {
96 | > .toggle { background-image: url(/images/treeView/group-closed-dark.svg); }
97 |
98 | + ol > ol, +ol > li { display: none; }
99 | }
100 | }
101 |
102 | .tree-loading {
103 | position: absolute;
104 | top: 0;
105 | bottom: 0;
106 | left: 0;
107 | right: 0;
108 | z-index: 1;
109 | display: flex;
110 | flex-flow: column;
111 | align-items: center;
112 | justify-content: center;
113 | text-align: center;
114 | text-transform: uppercase;
115 | opacity: 0.5;
116 | }
117 |
--------------------------------------------------------------------------------
/SupClient/styles/tabStrip.styl:
--------------------------------------------------------------------------------
1 | .tab-strip {
2 | height: 36px;
3 | display: flex;
4 | margin: 0 0 -1px -1px;
5 | padding: 0;
6 |
7 | * {
8 | -webkit-user-select: none;
9 | -moz-user-select: none;
10 | user-select: none;
11 | }
12 | }
13 |
14 | .tab-strip > li {
15 | flex: 0 1 160px;
16 | display: flex;
17 | align-items: center;
18 | width: 160px;
19 | height: 36px;
20 | background: #ddd;
21 | border-left: 1px solid #bbb;
22 | border-right: 1px solid #bbb;
23 | border-bottom: 1px solid #bbb;
24 | margin-right: -1px;
25 |
26 | &.dragged {
27 | flex: 0 0 0;
28 | position: absolute;
29 | z-index: 1000;
30 | }
31 |
32 | &.drop-placeholder {
33 | background: none;
34 | border-left: 1px solid rgba(0,0,0,0);
35 | border-right: 1px solid rgba(0,0,0,0);
36 | }
37 |
38 | &:not(.active) { color: #888; }
39 | &:hover { background: #eee; }
40 | &.active { background: #fff; border-bottom: 1px solid #fff; }
41 |
42 | &.unread {
43 | animation-duration: 1s;
44 | animation-iteration-count: infinite;
45 | animation-name: blink;
46 | background: #aaa;
47 | }
48 | }
49 |
50 | @keyframes blink {
51 | 0% { background: #fff; }
52 | 50% { background: #f7c9ae; }
53 | 100% { background: #fff; }
54 | }
55 |
56 | .tab-strip > li.pinned {
57 | flex: 0 1 40px;
58 | width: 40px;
59 | justify-content: center;
60 | }
61 |
62 | .tab-strip > li {
63 | .icon {
64 | padding: 0 0.25em;
65 | width: calc(24px + 0.5em);
66 | pointer-events: none;
67 | }
68 |
69 | .label {
70 | pointer-events: none;
71 | flex: 1 1 0;
72 | overflow: hidden;
73 | text-overflow: ellipsis;
74 | white-space: nowrap;
75 | display: flex;
76 | flex-flow: column;
77 | }
78 |
79 | .location {
80 | font-size: 10px;
81 | overflow: hidden;
82 | text-overflow: ellipsis;
83 | margin-bottom: -3px;
84 |
85 | &:empty { display: none; }
86 | }
87 |
88 | .name {
89 | overflow-x: hidden;
90 | text-overflow: ellipsis;
91 | }
92 |
93 | .close {
94 | margin-right: 0.25em;
95 | width: 20px;
96 | height: 20px;
97 | border: none;
98 | box-shadow: none;
99 | outline: none;
100 |
101 | background: url(/images/tabs/close.svg) center center no-repeat;
102 | opacity: 0.5;
103 |
104 | &:hover { background-image: url(/images/tabs/close-active.svg); opacity: 0.5; }
105 | &:active { background-image: url(/images/tabs/close-active.svg); opacity: 1; }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, and in the interest of
4 | fostering an open and welcoming community, we pledge to respect all people who
5 | contribute through reporting issues, posting feature requests, updating
6 | documentation, submitting pull requests or patches, and other activities.
7 |
8 | We are committed to making participation in this project a harassment-free
9 | experience for everyone, regardless of level of experience, gender, gender
10 | identity and expression, sexual orientation, disability, personal appearance,
11 | body size, race, ethnicity, age, religion, or nationality.
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | * The use of sexualized language or imagery
16 | * Personal attacks
17 | * Trolling or insulting/derogatory comments
18 | * Public or private harassment
19 | * Publishing other's private information, such as physical or electronic
20 | addresses, without explicit permission
21 | * Other unethical or unprofessional conduct
22 |
23 | Project maintainers have the right and responsibility to remove, edit, or
24 | reject comments, commits, code, wiki edits, issues, and other contributions
25 | that are not aligned to this Code of Conduct, or to ban temporarily or
26 | permanently any contributor for other behaviors that they deem inappropriate,
27 | threatening, offensive, or harmful.
28 |
29 | By adopting this Code of Conduct, project maintainers commit themselves to
30 | fairly and consistently applying these principles to every aspect of managing
31 | this project. Project maintainers who do not follow or enforce the Code of
32 | Conduct may be permanently removed from the project team.
33 |
34 | This Code of Conduct applies both within project spaces and in public spaces
35 | when an individual is representing the project or its community.
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
38 | reported by contacting a project maintainer at [team@sparklinlabs.com](mailto:team@sparklinlabs.com).
39 | All complaints will be reviewed and investigated and will result in a response
40 | that is deemed necessary and appropriate to the circumstances. Maintainers are
41 | obligated to maintain confidentiality with regard to the reporter of an
42 | incident.
43 |
44 |
45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
46 | version 1.3.0, available at
47 | [http://contributor-covenant.org/version/1/3/0/][version]
48 |
49 | [homepage]: http://contributor-covenant.org
50 | [version]: http://contributor-covenant.org/version/1/3/0/
51 |
--------------------------------------------------------------------------------
/SupClient/styles/Roboto.styl:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Roboto'; font-style: normal; font-weight: 100;
3 | src: local('Roboto Thin'), local('Roboto-Thin'), url(/fonts/Roboto/Roboto-Thin.woff) format('woff'), url(fonts/Roboto/Roboto-Thin.woff) format('woff');
4 | }
5 |
6 | @font-face {
7 | font-family: 'Roboto'; font-style: normal; font-weight: 300;
8 | src: local('Roboto Light'), local('Roboto-Light'), url(/fonts/Roboto/Roboto-Light.woff) format('woff'), url(fonts/Roboto/Roboto-Light.woff) format('woff');
9 | }
10 |
11 | @font-face {
12 | font-family: 'Roboto'; font-style: normal; font-weight: 400;
13 | src: local('Roboto'), local('Roboto-Regular'), url(/fonts/Roboto/Roboto.woff) format('woff'), url(fonts/Roboto/Roboto.woff) format('woff');
14 | }
15 |
16 | @font-face {
17 | font-family: 'Roboto'; font-style: normal; font-weight: 700;
18 | src: local('Roboto Bold'), local('Roboto-Bold'), url(/fonts/Roboto/Roboto-Bold.woff) format('woff'), url(fonts/Roboto/Roboto-Bold.woff) format('woff');
19 | }
20 |
21 | @font-face {
22 | font-family: 'Roboto'; font-style: normal; font-weight: 900;
23 | src: local('Roboto Black'), local('Roboto-Black'), url(/fonts/Roboto/Roboto-Black.woff) format('woff'), url(fonts/Roboto/Roboto-Black.woff) format('woff');
24 | }
25 |
26 | @font-face {
27 | font-family: 'Roboto'; font-style: italic; font-weight: 100;
28 | src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), url(/fonts/Roboto/Roboto-Thin-Italic.woff) format('woff'), url(fonts/Roboto/Roboto-Thin-Italic.woff) format('woff');
29 | }
30 |
31 | @font-face {
32 | font-family: 'Roboto'; font-style: italic; font-weight: 300;
33 | src: local('Roboto Light Italic'), local('Roboto-LightItalic'), url(/fonts/Roboto/Roboto-Light-Italic.woff) format('woff'), url(fonts/Roboto/Roboto-Light-Italic.woff) format('woff');
34 | }
35 |
36 | @font-face {
37 | font-family: 'Roboto'; font-style: italic; font-weight: 400;
38 | src: local('Roboto Italic'), local('Roboto-Italic'), url(/fonts/Roboto/Roboto-Italic.woff) format('woff'), url(fonts/Roboto/Roboto-Italic.woff) format('woff');
39 | }
40 |
41 | @font-face {
42 | font-family: 'Roboto'; font-style: italic; font-weight: 700;
43 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(/fonts/Roboto/Roboto-Bold-Italic.woff) format('woff'), url(fonts/Roboto/Roboto-Bold-Italic.woff) format('woff');
44 | }
45 |
46 | @font-face {
47 | font-family: 'Roboto'; font-style: italic; font-weight: 900;
48 | src: local('Roboto Black Italic'), local('Roboto-BlackItalic'), url(/fonts/Roboto/Roboto-Black-Italic.woff) format('woff'), url(fonts/Roboto/Roboto-Black-Italic.woff) format('woff');
49 | }
50 |
--------------------------------------------------------------------------------
/public/images/project/run.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
69 |
--------------------------------------------------------------------------------
/public/images/project/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers",
3 | "description": "Superpowers, the HTML5 2D+3D game maker",
4 | "version": "5.0.0",
5 | "license": "ISC",
6 | "main": "./server/index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/superpowers/superpowers-core.git"
10 | },
11 | "scripts": {
12 | "build": "node scripts/build.js",
13 | "start": "node server start",
14 | "package": "node scripts/package.js"
15 | },
16 | "superpowers": {
17 | "appApiVersion": 5
18 | },
19 | "devDependencies": {
20 | "@types/async": "^2.4.1",
21 | "@types/async-lock": "^1.1.1",
22 | "@types/basic-auth": "^1.1.2",
23 | "@types/express": "^4.16.1",
24 | "@types/express-rate-limit": "^3.3.0",
25 | "@types/js-cookie": "^2.2.1",
26 | "@types/lodash": "^4.14.122",
27 | "@types/mkdirp": "^0.5.2",
28 | "@types/node": "^11.10.4",
29 | "@types/passport": "^1.0.0",
30 | "@types/passport-local": "^1.0.33",
31 | "@types/recursive-readdir": "^2.2.0",
32 | "@types/rimraf": "^2.0.2",
33 | "@types/socket.io": "^2.1.2",
34 | "@types/socket.io-client": "^1.4.32",
35 | "@types/tv4": "^1.2.29",
36 | "brfs": "^2.0.2",
37 | "browserify": "^16.2.3",
38 | "chalk": "^1.1.3",
39 | "electron": "^4.0.7",
40 | "gulp": "^4.0.0",
41 | "gulp-concat-css": "^2.2.0",
42 | "gulp-pug": "^4.0.1",
43 | "gulp-rename": "^1.4.0",
44 | "gulp-stylus": "^2.7.0",
45 | "gulp-tslint": "^8.1.4",
46 | "gulp-typescript": "^5.0.0",
47 | "gulp-util": "^3.0.7",
48 | "simple-dialogs": "^2.1.2",
49 | "socket.io-client": "^2.2.0",
50 | "tslint": "^5.13.1",
51 | "vinyl-source-stream": "^2.0.0",
52 | "watchify": "^3.11.1"
53 | },
54 | "dependencies": {
55 | "@types/cookie-parser": "^1.4.1",
56 | "@types/express-session": "^1.15.12",
57 | "@types/yargs": "^12.0.9",
58 | "async": "^1.5.2",
59 | "async-lock": "^0.3.8",
60 | "basic-auth": "^2.0.1",
61 | "body-parser": "^1.15.2",
62 | "cookie-parser": "^1.4.1",
63 | "dnd-tree-view": "^4.0.2",
64 | "express": "^4.13.3",
65 | "express-rate-limit": "^3.4.0",
66 | "express-session": "^1.13.0",
67 | "follow-redirects": "0.0.7",
68 | "fuzzysort": "^1.1.4",
69 | "js-cookie": "^2.1.0",
70 | "lodash": "^4.17.15",
71 | "mkdirp": "^0.5.1",
72 | "passport": "^0.4.0",
73 | "passport-local": "^1.0.0",
74 | "passport.socketio": "^3.7.0",
75 | "recursive-readdir": "^2.2.2",
76 | "resize-handle": "^5.0.0",
77 | "rimraf": "^2.6.3",
78 | "socket.io": "^2.2.0",
79 | "tab-strip": "^2.3.0",
80 | "tsscmp": "^1.0.6",
81 | "tv4": "^1.2.7",
82 | "typescript": "^3.3.3333",
83 | "yargs": "^3.32.0",
84 | "yauzl": "^2.4.1"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/public/images/actions/filter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
83 |
--------------------------------------------------------------------------------
/public/images/project/build.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
74 |
--------------------------------------------------------------------------------
/public/images/tabs/close-active.svg:
--------------------------------------------------------------------------------
1 |
2 |
74 |
--------------------------------------------------------------------------------
/scripts/pluginGulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const gulp = require("gulp");
4 | const fs = require("fs");
5 |
6 | let editors = [];
7 | try { editors = fs.readdirSync("./editors"); } catch (err) { /* Ignore */ }
8 |
9 | const tasks = [];
10 |
11 | if (editors.length > 0) {
12 | // Pug
13 | const pug = require("gulp-pug");
14 | const rename = require("gulp-rename");
15 |
16 | const i18n = require("./i18n");
17 | const languageCodes = fs.readdirSync(i18n.rootLocalesPath);
18 | languageCodes.push("none");
19 |
20 | for (const languageCode of languageCodes) {
21 | const locale = i18n.loadLocale(languageCode, true);
22 | gulp.task(`pug-${languageCode}`, () => {
23 | let result = gulp.src("./editors/**/index.pug").pipe(pug({ locals: { t: i18n.makeT(locale) } }));
24 | if (languageCode !== "en") result = result.pipe(rename({ extname: `.${languageCode}.html` }));
25 | return result.pipe(gulp.dest("./public/editors"));
26 | });
27 | tasks.push(`pug-${languageCode}`);
28 | }
29 |
30 | // Stylus
31 | const stylus = require("gulp-stylus");
32 | gulp.task("stylus", () => gulp.src("./editors/**/index.styl").pipe(stylus({ errors: true, compress: true })).pipe(gulp.dest("./public/editors")));
33 | tasks.push("stylus");
34 | }
35 |
36 | // TypeScript
37 | const ts = require("gulp-typescript");
38 | const tsProject = ts.createProject("./tsconfig.json");
39 | const tslint = require("gulp-tslint");
40 |
41 | gulp.task("typescript", () => {
42 | const tsResult = tsProject.src()
43 | .pipe(tslint({ formatter: "prose" }))
44 | .pipe(tslint.report({ emitError: true }))
45 | .on("error", (err) => { throw err; })
46 | .pipe(tsProject())
47 | return tsResult.js.pipe(gulp.dest("./"));
48 | });
49 |
50 | // Browserify
51 | const browserify = require("browserify");
52 | const source = require("vinyl-source-stream");
53 |
54 | function makeBrowserify(src, dest, output) {
55 | gulp.task(`${output}-browserify`, () => {
56 | if (!fs.existsSync(src)) return Promise.resolve("No source.");
57 |
58 | return browserify(src)
59 | .transform("brfs").bundle()
60 | .pipe(source(`${output}.js`))
61 | .pipe(gulp.dest(dest));
62 | });
63 | tasks.push(`${output}-browserify`);
64 | }
65 |
66 | if (fs.existsSync("./public/bundles")) {
67 | for (const bundle of fs.readdirSync("./public/bundles")) fs.unlinkSync(`./public/bundles/${bundle}`);
68 | }
69 |
70 | const nonBundledFolders = [ "public", "editors", "node_modules", "typings" ];
71 | for (const folder of fs.readdirSync("./")) {
72 | if (nonBundledFolders.indexOf(folder) !== -1) continue;
73 |
74 | if (fs.existsSync(`./${folder}/index.ts`) || fs.existsSync(`./${folder}/index.js`))
75 | makeBrowserify(`./${folder}/index.js`, "./public/bundles", folder);
76 | }
77 |
78 | for (const editor of editors) makeBrowserify(`./editors/${editor}/index.js`, "./public/editors", `${editor}/index`);
79 |
80 | // All
81 | gulp.task("default", gulp.series("typescript", gulp.parallel(tasks)));
82 |
--------------------------------------------------------------------------------
/SupCore/systems.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 |
4 | export let systems: { [system: string]: System } = {};
5 |
6 | function shouldIgnoreFolder(pluginName: string) { return pluginName.indexOf(".") !== -1 || pluginName === "node_modules"; }
7 |
8 | export class System {
9 | data: SystemData;
10 |
11 | pluginsInfo: SupCore.PluginsInfo;
12 | serverBuild: (server: ProjectServer, buildPath: string, callback: (err: string) => void) => void;
13 |
14 | private plugins: { [contextName: string]: { [pluginName: string]: any; } } = {};
15 |
16 | constructor(public id: string, public folderName: string) {
17 | this.data = new SystemData(this);
18 | }
19 |
20 | requireForAllPlugins(filePath: string) {
21 | const pluginsPath = path.resolve(`${SupCore.systemsPath}/${this.folderName}/plugins`);
22 |
23 | for (const pluginAuthor of fs.readdirSync(pluginsPath)) {
24 | const pluginAuthorPath = `${pluginsPath}/${pluginAuthor}`;
25 | if (shouldIgnoreFolder(pluginAuthor)) continue;
26 |
27 | for (const pluginName of fs.readdirSync(pluginAuthorPath)) {
28 | if (shouldIgnoreFolder(pluginName)) continue;
29 |
30 | const completeFilePath = `${pluginAuthorPath}/${pluginName}/${filePath}`;
31 | if (fs.existsSync(completeFilePath)) {
32 | /* tslint:disable */
33 | require(completeFilePath);
34 | /* tslint:enable */
35 | }
36 | }
37 | }
38 | }
39 |
40 | registerPlugin(contextName: string, pluginName: string, plugin: T) {
41 | if (this.plugins[contextName] == null) this.plugins[contextName] = {};
42 |
43 | if (this.plugins[contextName][pluginName] != null) {
44 | console.error("SupCore.system.registerPlugin: Tried to register two or more plugins " +
45 | `named "${pluginName}" in context "${contextName}", system "${this.id}"`);
46 | }
47 |
48 | this.plugins[contextName][pluginName] = plugin;
49 | }
50 |
51 | getPlugins(contextName: string): { [pluginName: string]: T } {
52 | return this.plugins[contextName];
53 | }
54 | }
55 |
56 | class SystemData {
57 | assetClasses: { [assetName: string]: SupCore.Data.AssetClass; } = {};
58 | resourceClasses: { [resourceId: string]: SupCore.Data.ResourceClass; } = {};
59 |
60 | constructor(public system: System) {}
61 |
62 | registerAssetClass(name: string, assetClass: SupCore.Data.AssetClass) {
63 | if (this.assetClasses[name] != null) {
64 | console.log(`SystemData.registerAssetClass: Tried to register two or more asset classes named "${name}" in system "${this.system.id}"`);
65 | return;
66 | }
67 | this.assetClasses[name] = assetClass;
68 | return;
69 | }
70 |
71 | registerResource(id: string, resourceClass: SupCore.Data.ResourceClass) {
72 | if (this.resourceClasses[id] != null) {
73 | console.log(`SystemData.registerResource: Tried to register two or more plugin resources named "${id}" in system "${this.system.id}"`);
74 | return;
75 | }
76 | this.resourceClasses[id] = resourceClass;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/public/images/project/go-to-hub.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
78 |
--------------------------------------------------------------------------------
/server/ProjectHub.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import * as async from "async";
4 |
5 | import ProjectServer from "./ProjectServer";
6 | import RemoteHubClient from "./RemoteHubClient";
7 |
8 | export default class ProjectHub {
9 |
10 | globalIO: SocketIO.Server;
11 | io: SocketIO.Namespace;
12 | projectsPath: string;
13 | buildsPath: string;
14 |
15 | data = {
16 | projects: null as SupCore.Data.Projects
17 | };
18 |
19 | serversById: { [serverId: string]: ProjectServer } = {};
20 | loadingProjectFolderName: string;
21 |
22 | constructor(globalIO: SocketIO.Server, dataPath: string, callback: (err: Error) => any) {
23 | this.globalIO = globalIO;
24 | this.projectsPath = path.join(dataPath, "projects");
25 | this.buildsPath = path.join(dataPath, "builds");
26 |
27 | const serveProjects = (callback: async.ErrorCallback) => {
28 | async.eachSeries(fs.readdirSync(this.projectsPath), (folderName: string, cb: (err: Error) => any) => {
29 | if (folderName.indexOf(".") !== -1) { cb(null); return; }
30 | this.loadingProjectFolderName = folderName;
31 | this.loadProject(folderName, cb);
32 | }, (err) => {
33 | if (err != null) throw err;
34 | this.loadingProjectFolderName = null;
35 | callback();
36 | });
37 | };
38 |
39 | const setupProjectsList = (callback: Function) => {
40 | const data: SupCore.Data.ProjectManifestPub[] = [];
41 | for (const id in this.serversById) data.push(this.serversById[id].data.manifest.pub);
42 |
43 | data.sort(SupCore.Data.Projects.sort);
44 | this.data.projects = new SupCore.Data.Projects(data);
45 | callback();
46 | };
47 |
48 | const serve = (callback: Function) => {
49 | this.io = this.globalIO.of("/hub");
50 |
51 | this.io.on("connection", this.onAddSocket);
52 | callback();
53 | };
54 |
55 | async.waterfall([ serveProjects, setupProjectsList, serve ], callback);
56 | }
57 |
58 | saveAll(callback: (err: Error) => any) {
59 | async.each(Object.keys(this.serversById), (id, cb) => {
60 | this.serversById[id].save(cb);
61 | }, callback);
62 | }
63 |
64 | loadProject(folderName: string, callback: (err: Error) => any) {
65 | const server = new ProjectServer(this.globalIO, `${this.projectsPath}/${folderName}`, this.buildsPath, (err) => {
66 | if (err != null) { callback(err); return; }
67 |
68 | if (this.serversById[server.data.manifest.pub.id] != null) {
69 | callback(new Error(`There's already a project with this ID: ${server.data.manifest.pub.id} ` +
70 | `(${server.projectPath} and ${this.serversById[server.data.manifest.pub.id].projectPath})`));
71 | return;
72 | }
73 |
74 | this.serversById[server.data.manifest.pub.id] = server;
75 | callback(null);
76 | });
77 | }
78 |
79 | removeRemoteClient(socketId: string) {
80 | // this.clients.splice ...
81 | }
82 |
83 | private onAddSocket = (socket: SocketIO.Socket) => {
84 | /* const client = */ new RemoteHubClient(this, socket);
85 | // this.clients.push(client);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/SupCore/Data/Base/Dictionary.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "events";
2 |
3 | export default class Dictionary extends EventEmitter {
4 | byId: { [key: string]: any; };
5 | refCountById: { [key: string]: number; };
6 | unloadDelaySeconds: number;
7 | unloadTimeoutsById: {[id: string]: NodeJS.Timer} = {};
8 |
9 | constructor(unloadDelaySeconds = 60) {
10 | super();
11 | this.byId = {};
12 | this.refCountById = {};
13 | this.unloadDelaySeconds = unloadDelaySeconds;
14 | }
15 |
16 | acquire(id: string, owner: any, callback: (err: Error, item: any) => any) {
17 | if (this.refCountById[id] == null) this.refCountById[id] = 0;
18 | this.refCountById[id]++;
19 | // console.log(`Acquiring ${id}: ${this.refCountById[id]} refs`);
20 |
21 | // Cancel pending unload timeout if any
22 | const timeout = this.unloadTimeoutsById[id];
23 | if (timeout != null) {
24 | // console.log(`Cancelling unload timeout for ${id}`);
25 | clearTimeout(timeout);
26 | delete this.unloadTimeoutsById[id];
27 | }
28 |
29 | let item = this.byId[id];
30 |
31 | if (item == null) {
32 | try { item = this._load(id); }
33 | catch (e) { callback(e, null); return; }
34 | this.byId[id] = item;
35 |
36 | item.on("load", () => {
37 | // Bail if entry was evicted from the cache
38 | if (this.byId[id] == null) return;
39 | this.emit("itemLoad", id, item);
40 | });
41 | }
42 |
43 | if (item.pub != null) callback(null, item);
44 | else item.on("load", () => {
45 | // Bail if entry was evicted from the cache
46 | if (this.byId[id] == null) return;
47 | callback(null, item);
48 | return;
49 | });
50 | }
51 |
52 | release(id: string, owner: any, options?: {skipUnloadDelay: boolean}) {
53 | if (this.refCountById[id] == null) {
54 | // This might happen if .releaseAll(id) was called elsewhere since we called acquire
55 | // Just log and ignore
56 | console.log(`Can't release ${id}, ref count is null`);
57 | return;
58 | }
59 |
60 | this.refCountById[id]--;
61 | // console.log(`Releasing ${id}: ${this.refCountById[id]} refs left`);
62 |
63 | if (this.refCountById[id] === 0) {
64 | delete this.refCountById[id];
65 |
66 | // Schedule unloading the asset after a while
67 | if (options != null && options.skipUnloadDelay) this._unload(id);
68 | else this.unloadTimeoutsById[id] = setTimeout(() => { this._unload(id); }, this.unloadDelaySeconds * 1000);
69 | }
70 | }
71 |
72 | _load(id: string) { throw new Error("This method must be overridden by derived classes"); }
73 |
74 | _unload(id: string) {
75 | // console.log(`Unloading ${id}`);
76 | this.byId[id].unload();
77 | delete this.byId[id];
78 | delete this.unloadTimeoutsById[id];
79 | }
80 |
81 | releaseAll(id: string) {
82 | // Cancel pending unload timeout if any
83 | const timeout = this.unloadTimeoutsById[id];
84 | if (timeout != null) {
85 | clearTimeout(timeout);
86 | delete this.unloadTimeoutsById[id];
87 | }
88 |
89 | delete this.refCountById[id];
90 | delete this.byId[id];
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/server/commands/uninstall.ts:
--------------------------------------------------------------------------------
1 | import * as readline from "readline";
2 | import * as rimraf from "rimraf";
3 | import * as fs from "fs";
4 |
5 | import * as utils from "./utils";
6 |
7 | export default function uninstall(systemId: string, pluginFullName: string) {
8 | const localSystem = utils.systemsById[systemId];
9 | if (localSystem == null) utils.emitError(`System ${systemId} is not installed.`);
10 |
11 | if (pluginFullName == null) {
12 | // Uninstall system
13 | if (localSystem.isDev) utils.emitError(`System ${systemId} is a development version.`);
14 |
15 | if (utils.force) {
16 | uninstallSystem(localSystem.folderName);
17 | return;
18 | }
19 |
20 | const r1 = readline.createInterface({ input: process.stdin, output: process.stdout });
21 | r1.question(`Are you sure you want to uninstall the system ${systemId}? (yes/no): `, (answer) => {
22 | if (answer === "yes") {
23 | console.log(`Uninstalling system ${systemId}...`);
24 | uninstallSystem(localSystem.folderName);
25 | } else {
26 | console.log(`Uninstall canceled.`);
27 | process.exit(0);
28 | }
29 | });
30 |
31 | } else {
32 | // Uninstall plugin
33 | const [ authorName, pluginName ] = pluginFullName.split("/");
34 | if (utils.builtInPluginAuthors.indexOf(authorName) !== -1) utils.emitError(`Built-in plugins can not be uninstalled.`);
35 |
36 | const localPlugin = localSystem.plugins[authorName] != null ? localSystem.plugins[authorName][pluginName] : null;
37 | if (localPlugin == null) utils.emitError(`Plugin ${pluginFullName} is not installed.`);
38 |
39 | if (localPlugin.isDev) utils.emitError(`Plugin ${pluginFullName} is a development version.`);
40 |
41 | if (utils.force) {
42 | uninstallPlugin(localSystem.folderName, pluginFullName, authorName);
43 | return;
44 | }
45 |
46 | const r1 = readline.createInterface({ input: process.stdin, output: process.stdout });
47 | r1.question(`Are you sure you want to uninstall the plugin ${pluginFullName}? (yes/no): `, (answer) => {
48 | if (answer === "yes") {
49 | console.log(`Uninstalling plugin ${pluginFullName} from system ${systemId}...`);
50 | uninstallPlugin(localSystem.folderName, pluginFullName, authorName);
51 | } else {
52 | console.log(`Uninstall canceled.`);
53 | process.exit(0);
54 | }
55 | });
56 | }
57 | }
58 |
59 | function uninstallSystem(systemFolderName: string) {
60 | rimraf(`${utils.systemsPath}/${systemFolderName}`, (err) => {
61 | if (err != null) {
62 | utils.emitError(`Failed to uninstalled system.`);
63 | } else {
64 | console.log("System successfully uninstalled.");
65 | process.exit(0);
66 | }
67 | });
68 | }
69 |
70 | function uninstallPlugin(systemFolderName: string, pluginFullName: string, authorName: string) {
71 | rimraf(`${utils.systemsPath}/${systemFolderName}/plugins/${pluginFullName}`, (err) => {
72 | if (err != null) {
73 | utils.emitError(`Failed to uninstalled plugin.`);
74 | } else {
75 | if (fs.readdirSync(`${utils.systemsPath}/${systemFolderName}/plugins/${authorName}`).length === 0)
76 | fs.rmdirSync(`${utils.systemsPath}/${systemFolderName}/plugins/${authorName}`);
77 |
78 | console.log("Plugin successfully uninstalled.");
79 | process.exit(0);
80 | }
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/public/images/entries/filter-all.svg:
--------------------------------------------------------------------------------
1 |
2 |
86 |
--------------------------------------------------------------------------------
/server/migrateProject.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import * as async from "async";
4 | import * as mkdirp from "mkdirp";
5 |
6 | export default function(server: ProjectServer, callback: (err: Error) => any) {
7 | const oldVersion = server.data.manifest.migratedFromFormatVersion;
8 | if (oldVersion == null) { callback(null); return; }
9 |
10 | SupCore.log(`Migrating "${server.data.manifest.pub.name}" project (from format version ${oldVersion} to ${SupCore.Data.ProjectManifest.currentFormatVersion})...`);
11 |
12 | async.series([
13 | (cb) => { if (oldVersion < 1) migrateTo1(server, cb); else cb(); },
14 | (cb) => { if (oldVersion < 3) migrateTo3(server, cb); else cb(); }
15 | ], callback);
16 | }
17 |
18 | function migrateTo1(server: ProjectServer, callback: (err: Error) => any) {
19 | const assetsPath = path.join(server.projectPath, "assets");
20 |
21 | async.series([
22 | // Delete ArcadePhysics2DSettingsResource, removed in Superpowers v0.13
23 | // FIXME: This should be done by an init function in the plugin, probably.
24 | (cb) => { fs.unlink(path.join(server.projectPath, "resources/arcadePhysics2DSettings/resource.json"), (err) => { cb(); }); },
25 | (cb) => { fs.rmdir(path.join(server.projectPath, "resources/arcadePhysics2DSettings"), (err) => { cb(); }); },
26 |
27 | // Move trashed assets to "trashedAssets" folder
28 | (cb) => {
29 | fs.readdir(assetsPath, (err, assetFolders) => {
30 | if (err != null) throw err;
31 |
32 | const assetFolderRegex = /^[0-9]+-.+$/;
33 | const trashedAssetFolders: string[] = [];
34 | for (const assetFolder of assetFolders) {
35 | if (!assetFolderRegex.test(assetFolder)) continue;
36 |
37 | const assetId = assetFolder.substring(0, assetFolder.indexOf("-"));
38 | if (server.data.entries.byId[assetId] == null) trashedAssetFolders.push(assetFolder);
39 | }
40 |
41 | async.each(trashedAssetFolders, server.moveAssetFolderToTrash.bind(server), cb);
42 | });
43 | },
44 |
45 | // Delete internals.json and members.json
46 | (cb) => { fs.unlink(path.join(server.projectPath, "internals.json"), (err) => { cb(); }); },
47 | (cb) => { fs.unlink(path.join(server.projectPath, "members.json"), (err) => { cb(); }); }
48 | ], callback);
49 | }
50 |
51 | function migrateTo3(server: ProjectServer, callback: (err: Error) => any) {
52 | const assetsPath = path.join(server.projectPath, "assets");
53 |
54 | async.eachSeries(Object.keys(server.data.entries.byId), (nodeId, cb) => {
55 | const node = server.data.entries.byId[nodeId];
56 | const storagePath = server.data.entries.getStoragePathFromId(nodeId);
57 |
58 | if (node.type == null) cb();
59 | else {
60 | const index = storagePath.lastIndexOf("/");
61 | let parentStoragePath = storagePath;
62 | const oldStoragePath = path.join(assetsPath, `${nodeId}-${server.data.entries.getPathFromId(nodeId).replace(new RegExp("/", "g"), "__")}`);
63 |
64 | if (index !== -1) {
65 | parentStoragePath = storagePath.slice(0, index);
66 | mkdirp(path.join(assetsPath, parentStoragePath), (err) => {
67 | if (err != null && err.code !== "EEXIST") { cb(err); return; }
68 | fs.rename(oldStoragePath, path.join(assetsPath, storagePath), cb);
69 | });
70 | } else {
71 | fs.rename(oldStoragePath, path.join(assetsPath, storagePath), cb);
72 | }
73 | }
74 | }, callback);
75 | }
76 |
--------------------------------------------------------------------------------
/server/BaseRemoteClient.ts:
--------------------------------------------------------------------------------
1 | import * as AsyncLock from "async-lock";
2 |
3 | export default class BaseRemoteClient {
4 | subscriptions: string[] = [];
5 | lock = new AsyncLock();
6 |
7 | constructor(public server: BaseServer, public socket: SocketIO.Socket) {
8 | this.socket.on("error", (err: Error) => { SupCore.log((err as any).stack); });
9 | this.socket.on("disconnect", this.onDisconnect);
10 |
11 | this.socket.on("sub", this.onSubscribe);
12 | this.socket.on("unsub", this.onUnsubscribe);
13 | }
14 |
15 | errorIfCant(action: string, callback: (error: string) => any) {
16 | if (!this.can(action)) {
17 | if (callback != null) callback("Forbidden");
18 | return false;
19 | }
20 |
21 | return true;
22 | }
23 |
24 | can(action: string): boolean { throw new Error("BaseRemoteClient.can() must be overridden"); }
25 |
26 | /*
27 | _error(message: string) {
28 | this.socket.emit("error", message);
29 | this.socket.disconnect();
30 | }
31 | */
32 |
33 | private onDisconnect = () => {
34 | for (const subscription of this.subscriptions) {
35 | const [ , endpoint, id ] = subscription.split(":");
36 | if (id == null) continue;
37 |
38 | (this.server.data[endpoint] as SupCore.Data.Base.Dictionary).release(id, this);
39 | }
40 |
41 | this.server.removeRemoteClient(this.socket.id);
42 | }
43 |
44 | private onSubscribe = (endpoint: string, id: string, callback: (err: string, pubData?: any, optionalArg?: any) => any) => {
45 | const roomName = ((id != null) ? `sub:${endpoint}:${id}` : `sub:${endpoint}`);
46 |
47 | this.lock.acquire(roomName, (unlockRoom) => {
48 | const data = this.server.data[endpoint];
49 | if (data == null) {
50 | callback("No such endpoint");
51 | unlockRoom();
52 | return;
53 | }
54 |
55 | if (this.subscriptions.indexOf(roomName) !== -1) { callback(`You're already subscribed to ${id}`); return; }
56 |
57 | if (id == null) {
58 | this.socket.join(roomName);
59 | this.subscriptions.push(roomName);
60 | const pub = (data as SupCore.Data.Base.Hash).pub;
61 | const optionalArg = endpoint === "entries" ? (data as SupCore.Data.Entries).nextId : null;
62 | callback(null, pub, optionalArg);
63 | unlockRoom();
64 | return;
65 | }
66 |
67 | (data as SupCore.Data.Base.Dictionary).acquire(id, this, (err: Error, item: any) => {
68 | if (err != null) {
69 | callback(`Could not acquire item: ${err}`, null);
70 | unlockRoom();
71 | return;
72 | }
73 |
74 | this.socket.join(roomName);
75 | this.subscriptions.push(roomName);
76 |
77 | callback(null, item.pub);
78 | unlockRoom();
79 | return;
80 | });
81 | });
82 | }
83 |
84 | private onUnsubscribe = (endpoint: string, id: string) => {
85 | const data = this.server.data[endpoint];
86 | if (data == null) return;
87 |
88 | const roomName = ((id != null) ? `sub:${endpoint}:${id}` : `sub:${endpoint}`);
89 |
90 | this.lock.acquire(roomName, (unlockRoom) => {
91 | const index = this.subscriptions.indexOf(roomName);
92 | if (index === -1) return;
93 |
94 | if (id != null) { (data as SupCore.Data.Base.Dictionary).release(id, this); }
95 |
96 | this.socket.leave(roomName);
97 | this.subscriptions.splice(index, 1);
98 | unlockRoom();
99 | });
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/public/images/project/debug.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
78 |
--------------------------------------------------------------------------------
/public/images/controls/notifications-enabled.svg:
--------------------------------------------------------------------------------
1 |
2 |
72 |
--------------------------------------------------------------------------------
/server/commands/install.ts:
--------------------------------------------------------------------------------
1 | import * as utils from "./utils";
2 |
3 | export default function install(systemId: string, pluginFullName: string) {
4 | const localSystem = utils.systemsById[systemId];
5 |
6 | if (utils.downloadURL != null) {
7 | if (pluginFullName == null) {
8 | if (localSystem != null) utils.emitError(`System ${systemId} is already installed.`);
9 |
10 | installSystem(systemId, utils.downloadURL);
11 |
12 | } else {
13 | const [ authorName, pluginName ] = pluginFullName.split("/");
14 | const localPlugin = localSystem != null && localSystem.plugins[authorName] != null ? localSystem.plugins[authorName][pluginName] : null;
15 |
16 | if (localPlugin != null) utils.emitError(`Plugin ${pluginFullName} is already installed.`);
17 |
18 | installPlugin(systemId, pluginFullName, utils.downloadURL);
19 | }
20 | return;
21 | }
22 |
23 | utils.getRegistry((err, registry) => {
24 | if (err) utils.emitError("Error while fetching registry:", err.stack);
25 |
26 | const registrySystem = registry.systems[systemId];
27 | if (registrySystem == null) {
28 | console.error(`System ${systemId} is not on the registry.`);
29 | utils.listAvailableSystems(registry);
30 | process.exit(1);
31 | }
32 |
33 | if (localSystem != null) {
34 | if (pluginFullName == null) {
35 | console.error(`System ${systemId} is already installed.`);
36 | utils.listAvailableSystems(registry);
37 | process.exit(1);
38 | } else if (pluginFullName === "") {
39 | utils.listAvailablePlugins(registry, systemId);
40 | process.exit(0);
41 | }
42 |
43 | const [ authorName, pluginName ] = pluginFullName.split("/");
44 | const localPlugin = localSystem != null && localSystem.plugins[authorName] != null ? localSystem.plugins[authorName][pluginName] : null;
45 |
46 | const registryPlugin = registrySystem.plugins[authorName] != null ? registrySystem.plugins[authorName][pluginName] : null;
47 | if (registryPlugin == null) {
48 | console.error(`Plugin ${pluginFullName} is not on the registry.`);
49 | utils.listAvailablePlugins(registry, systemId);
50 | process.exit(1);
51 | }
52 |
53 | if (localPlugin != null) {
54 | console.error(`Plugin ${pluginFullName} is already installed.`);
55 | utils.listAvailablePlugins(registry, systemId);
56 | process.exit(1);
57 | }
58 |
59 | installPlugin(systemId, pluginFullName, registryPlugin.downloadURL);
60 | } else {
61 | if (pluginFullName != null) utils.emitError(`System ${systemId} is not installed.`);
62 |
63 | installSystem(systemId, registrySystem.downloadURL);
64 | }
65 | });
66 | }
67 |
68 | function installSystem(systemId: string, downloadURL: string) {
69 | console.log(`Installing system ${systemId}...`);
70 | const systemPath = `${utils.systemsPath}/${systemId}`;
71 |
72 | utils.downloadRelease(downloadURL, systemPath, (err) => {
73 | if (err != null) utils.emitError("Failed to install the system.", err);
74 |
75 | console.log("System successfully installed.");
76 | process.exit(0);
77 | });
78 | }
79 |
80 | function installPlugin(systemId: string, pluginFullName: string, downloadURL: string) {
81 | console.log(`Installing plugin ${pluginFullName} on system ${systemId}...`);
82 | const pluginPath = `${utils.systemsPath}/${utils.systemsById[systemId].folderName}/plugins/${pluginFullName}`;
83 | utils.downloadRelease(downloadURL, pluginPath, (err) => {
84 | if (err != null) utils.emitError("Failed to install the plugin.", err);
85 |
86 | console.log("Plugin successfully installed.");
87 | process.exit(0);
88 | });
89 | }
90 |
--------------------------------------------------------------------------------
/public/images/controls/notifications-disabled-hover.svg:
--------------------------------------------------------------------------------
1 |
2 |
81 |
--------------------------------------------------------------------------------
/SupCore/Data/Room.ts:
--------------------------------------------------------------------------------
1 | import * as SupData from "./index";
2 | import * as path from "path";
3 | import * as fs from "fs";
4 |
5 | export default class Room extends SupData.Base.Hash {
6 | static schema: SupCore.Data.Schema = {
7 | history: {
8 | type: "array",
9 | items: {
10 | type: "hash",
11 | properties: {
12 | timestamp: { type: "number" },
13 | author: { type: "string" },
14 | text: { type: "string" },
15 | users: { type: "array" }
16 | }
17 | }
18 | }
19 | };
20 |
21 | users: SupData.RoomUsers;
22 |
23 | constructor(pub: any) {
24 | super(pub, Room.schema);
25 |
26 | if (this.pub != null) this.users = new SupData.RoomUsers(this.pub.users);
27 | }
28 |
29 | load(roomPath: string) {
30 | fs.readFile(path.join(`${roomPath}.json`), { encoding: "utf8" }, (err, json) => {
31 | if (err != null && err.code !== "ENOENT") throw err;
32 |
33 | if (json == null) this.pub = { history: [] };
34 | else this.pub = JSON.parse(json);
35 |
36 | this.pub.users = [];
37 | this.users = new SupData.RoomUsers(this.pub.users);
38 |
39 | this.emit("load");
40 | });
41 | }
42 |
43 | unload() { this.removeAllListeners(); return; }
44 |
45 | save(roomPath: string, callback: (err: Error) => any) {
46 | const users = this.pub.users;
47 | delete this.pub.users;
48 | const json = JSON.stringify(this.pub, null, 2);
49 | this.pub.users = users;
50 |
51 | fs.writeFile(path.join(`${roomPath}.json`), json, { encoding: "utf8" }, callback);
52 | }
53 |
54 | join(client: SupCore.RemoteClient, callback: (err: string, item?: any, index?: number) => any) {
55 | const username = client.socket.request.user.username;
56 | let item = this.users.byId[username];
57 | if (item != null) {
58 | item.connectionCount++;
59 | callback(null, item);
60 | return;
61 | }
62 |
63 | item = { id: username, connectionCount: 1 };
64 |
65 | this.users.add(item, null, (err, actualIndex) => {
66 | if (err != null) { callback(err); return; }
67 | callback(null, item, actualIndex);
68 | });
69 | }
70 |
71 | client_join(item: any, index: number) {
72 | if (index != null) this.users.client_add(item, index);
73 | else this.users.byId[item.id].connectionCount++;
74 | }
75 |
76 | leave(client: SupCore.RemoteClient, callback: (err: string, username?: any) => any) {
77 | const username = client.socket.request.user.username;
78 | const item = this.users.byId[username];
79 | if (item.connectionCount > 1) {
80 | item.connectionCount--;
81 | callback(null, username);
82 | return;
83 | }
84 |
85 | this.users.remove(username, (err) => {
86 | if (err != null) { callback(err); return; }
87 | callback(null, username);
88 | });
89 | }
90 |
91 | client_leave(id: string) {
92 | const item = this.users.byId[id];
93 | if (item.connectionCount > 1) { item.connectionCount--; return; }
94 |
95 | this.users.client_remove(id);
96 | }
97 |
98 | server_appendMessage(client: SupCore.RemoteClient, text: string, callback: (err: string, entry?: any) => any) {
99 | if (typeof(text) !== "string" || text.length > 300) { callback("Your message was too long"); return; }
100 |
101 | const entry = { timestamp: Date.now(), author: client.socket.request.user.username, text: text };
102 | this.pub.history.push(entry);
103 | if (this.pub.history.length > 100) this.pub.history.splice(0, 1);
104 |
105 | callback(null, entry);
106 | this.emit("change");
107 | }
108 |
109 | client_appendMessage(entry: any) {
110 | this.pub.history.push(entry);
111 | if (this.pub.history.length > 100) this.pub.history.splice(0, 1);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/public/images/controls/notifications-disabled.svg:
--------------------------------------------------------------------------------
1 |
2 |
73 |
--------------------------------------------------------------------------------
/public/images/controls/notifications-enabled-hover.svg:
--------------------------------------------------------------------------------
1 |
2 |
80 |
--------------------------------------------------------------------------------