├── 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 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 50 | 55 | 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 | [![License](https://img.shields.io/badge/license-ISC-blue.svg)](https://github.com/superpowers/superpowers-core/blob/master/LICENSE.txt) 4 | [![Build Status](https://travis-ci.org/superpowers/superpowers-core.svg?branch=master)](https://travis-ci.org/superpowers/superpowers-core) 5 | [![Gitter](https://img.shields.io/gitter/room/superpowers/dev.svg)](https://gitter.im/superpowers/dev) 6 | [![Patreon](https://img.shields.io/badge/patreon-support%20us-brightgreen.svg)](https://www.patreon.com/SparklinLabs) 7 | [![Twitter](https://img.shields.io/twitter/follow/SuperpowersDev.svg?style=social)](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 | ![](http://i.imgur.com/g4iNlEn.png) 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 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /public/images/project/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 42 | 45 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 70 | 71 | 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 | 19 | 21 | 52 | 57 | 58 | 60 | 61 | 63 | image/svg+xml 64 | 66 | 67 | 68 | 69 | 70 | 75 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /public/images/project/build.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 48 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /public/images/tabs/close-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 50 | 57 | 58 | 68 | 73 | 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 | 19 | 21 | 49 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 76 | 77 | 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 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 56 | 59 | 60 | 67 | 72 | 79 | 85 | 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 | 19 | 21 | 42 | 45 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 69 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /public/images/controls/notifications-enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 55 | 58 | 59 | 65 | 71 | 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 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 37 | 38 | 64 | 67 | 68 | 75 | 80 | 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 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 56 | 59 | 60 | 68 | 72 | 73 | -------------------------------------------------------------------------------- /public/images/controls/notifications-enabled-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 56 | 59 | 60 | 67 | 73 | 79 | 80 | --------------------------------------------------------------------------------