diff --git a/app.py b/app.py index 29067f3..d26d7fa 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,10 @@ import base64 import bcrypt import hashlib -import config +try: + import config +except ModuleNotFoundError: + raise FileNotFoundError('No such file or directory: \'config.py\'. Copy the example config file config.example.py to config.py') import json import re import requests @@ -20,23 +23,32 @@ from ffmpy import FFmpeg from pymongo import MongoClient from redis import Redis -app = Flask(__name__) -client = MongoClient(host=config.MONGO['host']) +def take_config(name, required=False): + if hasattr(config, name): + return getattr(config, name) + elif required: + raise ValueError('Required option is not defined in the config.py file: {}'.format(name)) + else: + return None -app.secret_key = config.SECRET_KEY +app = Flask(__name__) +client = MongoClient(host=take_config('MONGO', required=True)['host']) + +app.secret_key = take_config('SECRET_KEY') or 'change-me' app.config['SESSION_TYPE'] = 'redis' +redis_config = take_config('REDIS', required=True) app.config['SESSION_REDIS'] = Redis( - host=config.REDIS['CACHE_REDIS_HOST'], - port=config.REDIS['CACHE_REDIS_PORT'], - password=config.REDIS['CACHE_REDIS_PASSWORD'], - db=config.REDIS['CACHE_REDIS_DB'] + host=redis_config['CACHE_REDIS_HOST'], + port=redis_config['CACHE_REDIS_PORT'], + password=redis_config['CACHE_REDIS_PASSWORD'], + db=redis_config['CACHE_REDIS_DB'] ) -app.cache = Cache(app, config=config.REDIS) +app.cache = Cache(app, config=redis_config) sess = Session() sess.init_app(app) csrf = CSRFProtect(app) -db = client[config.MONGO['database']] +db = client[take_config('MONGO', required=True)['database']] db.users.create_index('username', unique=True) db.songs.create_index('id', unique=True) db.scores.create_index('username') @@ -53,12 +65,12 @@ def api_error(message): def generate_hash(id, form): md5 = hashlib.md5() if form['type'] == 'tja': - urls = ['%s%s/main.tja' % (config.SONGS_BASEURL, id)] + urls = ['%s%s/main.tja' % (take_config('SONGS_BASEURL', required=True), id)] else: urls = [] for diff in ['easy', 'normal', 'hard', 'oni', 'ura']: if form['course_' + diff]: - urls.append('%s%s/%s.osu' % (config.SONGS_BASEURL, id, diff)) + urls.append('%s%s/%s.osu' % (take_config('SONGS_BASEURL', required=True), id, diff)) for url in urls: if url.startswith("http://") or url.startswith("https://"): @@ -117,22 +129,24 @@ def before_request_func(): def get_config(credentials=False): config_out = { - 'songs_baseurl': config.SONGS_BASEURL, - 'assets_baseurl': config.ASSETS_BASEURL, - 'email': config.EMAIL, - 'accounts': config.ACCOUNTS, - 'custom_js': config.CUSTOM_JS, - 'preview_type': config.PREVIEW_TYPE or 'mp3' + 'songs_baseurl': take_config('SONGS_BASEURL', required=True), + 'assets_baseurl': take_config('ASSETS_BASEURL', required=True), + 'email': take_config('EMAIL'), + 'accounts': take_config('ACCOUNTS'), + 'custom_js': take_config('CUSTOM_JS'), + 'plugins': take_config('PLUGINS') and [x for x in take_config('PLUGINS') if x['url']], + 'preview_type': take_config('PREVIEW_TYPE') or 'mp3' } if credentials: - min_level = config.GOOGLE_CREDENTIALS['min_level'] or 0 + google_credentials = take_config('GOOGLE_CREDENTIALS') + min_level = google_credentials['min_level'] or 0 if not session.get('username'): user_level = 0 else: user = db.users.find_one({'username': session.get('username')}) user_level = user['user_level'] if user_level >= min_level: - config_out['google_credentials'] = config.GOOGLE_CREDENTIALS + config_out['google_credentials'] = google_credentials else: config_out['google_credentials'] = { 'gdrive_enabled': False @@ -146,9 +160,8 @@ def get_config(credentials=False): config_out['_version'] = get_version() return config_out - def get_version(): - version = {'commit': None, 'commit_short': '', 'version': None, 'url': config.URL} + version = {'commit': None, 'commit_short': '', 'version': None, 'url': take_config('URL')} if os.path.isfile('version.json'): try: ver = json.load(open('version.json', 'r')) @@ -669,7 +682,7 @@ def route_api_scores_get(): @app.route('/privacy') def route_api_privacy(): last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt'))) - integration = config.GOOGLE_CREDENTIALS['gdrive_enabled'] + integration = take_config('GOOGLE_CREDENTIALS')['gdrive_enabled'] if take_config('GOOGLE_CREDENTIALS') else False response = make_response(render_template('privacy.txt', last_modified=last_modified, config=get_config(), integration=integration)) response.headers['Content-type'] = 'text/plain; charset=utf-8' diff --git a/config.example.py b/config.example.py index 311827b..c515839 100644 --- a/config.example.py +++ b/config.example.py @@ -13,6 +13,13 @@ ACCOUNTS = True # Custom JavaScript file to load with the simulator. CUSTOM_JS = '' +# Default plugins to load with the simulator. +PLUGINS = [{ + 'url': '', + 'start': False, + 'hide': False +}] + # Filetype to use for song previews. (mp3/ogg) PREVIEW_TYPE = 'mp3' diff --git a/public/src/css/view.css b/public/src/css/view.css index 640931c..7b20e5d 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -293,7 +293,7 @@ kbd{ #settings-latency .view{ width: 30em; } -#settings-latency .setting-value{ +.setting-value{ position: relative; } .setting-value:not(.selected) .latency-buttons{ diff --git a/public/src/js/browsersupport.js b/public/src/js/browsersupport.js index 20698eb..5555826 100644 --- a/public/src/js/browsersupport.js +++ b/public/src/js/browsersupport.js @@ -56,6 +56,10 @@ function browserSupport(){ }, "KeyboardEvent.key": function(){ return "key" in KeyboardEvent.prototype + }, + "Module import": function(){ + eval("import('data:text/javascript,')") + return true } } failedTests = [] @@ -107,10 +111,12 @@ function showUnsupported(strings){ var warn = document.createElement("div") warn.id = "unsupportedWarn" warn.innerText = "!" + warn.textContent = "!" div.appendChild(warn) var hide = document.createElement("div") hide.id = "unsupportedHide" hide.innerText = "x" + hide.textContent = "x" div.appendChild(hide) var span = document.createElement("span") @@ -119,6 +125,7 @@ function showUnsupported(strings){ if(i !== 0){ var link = document.createElement("a") link.innerText = strings.browserSupport.details + link.textContent = strings.browserSupport.details span.appendChild(link) } span.appendChild(document.createTextNode(browserWarning[i])) @@ -133,6 +140,7 @@ function showUnsupported(strings){ for(var i = 0; i < failedTests.length; i++){ var li = document.createElement("li") li.innerText = failedTests[i] + li.textContent = failedTests[i] ul.appendChild(li) } details.appendChild(ul) @@ -143,6 +151,7 @@ function showUnsupported(strings){ var chrome = document.createElement("a") chrome.href = "https://www.google.com/chrome/" chrome.innerText = "Google Chrome" + chrome.textContent = "Google Chrome" details.appendChild(chrome) } details.appendChild(document.createTextNode(supportedBrowser[i])) diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js index ba199ae..40c29b7 100644 --- a/public/src/js/importsongs.js +++ b/public/src/js/importsongs.js @@ -108,7 +108,10 @@ ) ))){ this.plugins.forEach(obj => { - var plugin = plugins.add(obj.data, obj.name) + var plugin = plugins.add(obj.data, { + name: obj.name, + raw: true + }) if(plugin){ pluginAmount++ plugin.imported = true diff --git a/public/src/js/loader.js b/public/src/js/loader.js index f57960e..eba7371 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -183,7 +183,7 @@ class Loader{ var image = document.createElement("img") var url = gameConfig.assets_baseurl + "img/" + name categoryPromises.push(pageEvents.load(image).catch(response => { - this.errorMsg(response, url) + return this.errorMsg(response, url) })) image.id = name image.src = url @@ -325,6 +325,26 @@ class Loader{ promises.push(this.canvasTest.drawAllImages()) + if(gameConfig.plugins){ + gameConfig.plugins.forEach(obj => { + if(obj.url){ + var plugin = plugins.add(obj.url, { + hide: obj.hide + }) + if(plugin){ + plugin.loadErrors = true + promises.push(plugin.load(true).then(() => { + if(obj.start){ + plugin.start() + } + }, response => { + return this.errorMsg(response, obj.url) + })) + } + } + }) + } + Promise.all(promises).then(result => { perf.allImg = result perf.load = Date.now() - this.startTime @@ -332,8 +352,8 @@ class Loader{ this.clean() this.callback(songId) pageEvents.send("ready", readyEvent) - }) - }, this.errorMsg.bind(this)) + }, () => this.errorMsg()) + }, () => this.errorMsg()) }) } addPromise(promise, url){ @@ -433,6 +453,7 @@ class Loader{ } var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount)) this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```" + return Promise.reject(error) } assetLoaded(){ if(!this.error){ diff --git a/public/src/js/p2.js b/public/src/js/p2.js index f8bcc69..3025334 100644 --- a/public/src/js/p2.js +++ b/public/src/js/p2.js @@ -11,6 +11,7 @@ class P2Connection{ this.allEvents = new Map() this.addEventListener("message", this.message.bind(this)) this.currentHash = "" + this.disabled = 0 pageEvents.add(window, "hashchange", this.onhashchange.bind(this)) } addEventListener(type, callback){ @@ -257,11 +258,11 @@ class P2Connection{ } } enable(){ - this.disabled = false - this.open() + this.disabled = Math.max(0, this.disabled - 1) + setTimeout(this.open.bind(this), 100) } disable(){ - this.disabled = true + this.disabled++ this.close() } } diff --git a/public/src/js/plugins.js b/public/src/js/plugins.js index 30a02d7..df0f983 100644 --- a/public/src/js/plugins.js +++ b/public/src/js/plugins.js @@ -8,18 +8,39 @@ class Plugins{ this.hashes = [] this.startOrder = [] } - add(script, name){ + add(script, options){ + options = options || {} var hash = md5.base64(script.toString()) + var isUrl = typeof script === "string" && !options.raw + if(isUrl){ + hash = "url " + hash + }else if(typeof script !== "string"){ + hash = "class " + hash + } + var name = options.name + if(!name && isUrl){ + name = script + var index = name.lastIndexOf("/") + if(index !== -1){ + name = name.slice(index + 1) + } + if(name.endsWith(".taikoweb.js")){ + name = name.slice(0, -".taikoweb.js".length) + }else if(name.endsWith(".js")){ + name = name.slice(0, -".js".length) + } + } + name = name || "plugin" if(this.hashes.indexOf(hash) !== -1){ console.warn("Skip adding an already addded plugin: " + name) return } - name = name || "plugin" var baseName = name for(var i = 2; name in this.pluginMap; i++){ name = baseName + i.toString() } - var plugin = new PluginLoader(script, name, hash) + var plugin = new PluginLoader(script, name, hash, options.raw) + plugin.hide = !!options.hide this.allPlugins.push({ name: name, plugin: plugin @@ -41,10 +62,14 @@ class Plugins{ if(index !== -1){ this.allPlugins.splice(index, 1) } + var index = this.startOrder.indexOf(name) + if(index !== -1){ + this.startOrder.splice(index, 1) + } delete this.pluginMap[name] } load(name){ - this.pluginMap[name].load() + return this.pluginMap[name].load() } loadAll(){ for(var i = 0; i < this.allPlugins.length; i++){ @@ -52,7 +77,7 @@ class Plugins{ } } start(name){ - this.pluginMap[name].start() + return this.pluginMap[name].start() } startAll(){ for(var i = 0; i < this.allPlugins.length; i++){ @@ -60,7 +85,7 @@ class Plugins{ } } stop(name){ - this.pluginMap[name].stop() + return this.pluginMap[name].stop() } stopAll(){ for(var i = this.startOrder.length; i--;){ @@ -68,7 +93,7 @@ class Plugins{ } } unload(name){ - this.pluginMap[name].unload() + return this.pluginMap[name].unload() } unloadAll(){ for(var i = this.startOrder.length; i--;){ @@ -127,30 +152,79 @@ class Plugins{ } getSettings(){ - var items = {} + var items = [] for(var i = 0; i < this.allPlugins.length; i++){ var obj = this.allPlugins[i] let plugin = obj.plugin - items[obj.name] = { - name: plugin.module ? this.getLocalTitle(plugin.module.name || obj.name, plugin.module.name_lang) : obj.name, - type: "toggle", - default: true, - getItem: () => plugin.started, - setItem: value => { - if(plugin.started && !value){ - this.stop(plugin.name) - }else if(!plugin.started && value){ - this.start(plugin.name) - } + if(!plugin.loaded){ + continue + } + if(!plugin.hide){ + let description + let description_lang + var module = plugin.module + if(module){ + description = [ + module.description, + module.author ? strings.plugins.author.replace("%s", module.author) : null, + module.version ? strings.plugins.version.replace("%s", module.version) : null + ].filter(Boolean).join("\n") + description_lang = {} + languageList.forEach(lang => { + description_lang[lang] = [ + this.getLocalTitle(module.description, module.description_lang, lang), + module.author ? allStrings[lang].plugins.author.replace("%s", module.author) : null, + module.version ? allStrings[lang].plugins.version.replace("%s", module.version) : null + ].filter(Boolean).join("\n") + }) } + var name = module && module.name || obj.name + var name_lang = module && module.name_lang + items.push({ + name: name, + name_lang: name_lang, + description: description, + description_lang: description_lang, + type: "toggle", + default: true, + getItem: () => plugin.started, + setItem: value => { + if(plugin.started && !value){ + this.stop(plugin.name) + }else if(!plugin.started && value){ + this.start(plugin.name) + } + } + }) + } + var settings = plugin.settings() + if(settings){ + settings.forEach(setting => { + if(!setting.name){ + setting.name = name + if(!setting.name_lang){ + setting.name_lang = name_lang + } + } + if(typeof setting.getItem !== "function"){ + setting.getItem = () => {} + } + if(typeof setting.setItem !== "function"){ + setting.setItem = () => {} + } + if(!("indent" in setting) && !plugin.hide){ + setting.indent = 1 + } + items.push(setting) + }) } } return items } - getLocalTitle(title, titleLang){ + getLocalTitle(title, titleLang, lang){ if(titleLang){ for(var id in titleLang){ - if(id === strings.id && titleLang[id]){ + if(id === (lang || strings.id) && titleLang[id]){ return titleLang[id] } } @@ -163,20 +237,30 @@ class PluginLoader{ constructor(...args){ this.init(...args) } - init(script, name, hash){ + init(script, name, hash, raw){ this.name = name this.hash = hash if(typeof script === "string"){ - this.url = URL.createObjectURL(new Blob([script], { - type: "application/javascript" - })) + if(raw){ + this.url = URL.createObjectURL(new Blob([script], { + type: "application/javascript" + })) + }else{ + this.url = script + } }else{ this.class = script } } - load(){ - if(this.loaded || !this.url && !this.class){ + load(loadErrors){ + if(this.loaded){ return Promise.resolve() + }else if(!this.url && !this.class){ + if(loadErrors){ + return Promise.reject() + }else{ + return Promise.resolve() + } }else{ return (this.url ? import(this.url) : Promise.resolve({ default: this.class @@ -209,7 +293,11 @@ class PluginLoader{ }, e => { console.error(e) this.error() - return Promise.resolve() + if(loadErrors){ + return Promise.reject(e) + }else{ + return Promise.resolve() + } }) } } @@ -300,6 +388,20 @@ class PluginLoader{ } this.unload(true) } + settings(){ + if(this.module && this.module.settings){ + try{ + var settings = this.module.settings() + }catch(e){ + console.error(e) + this.error() + return + } + if(Array.isArray(settings)){ + return settings + } + } + } } class EditValue{ @@ -308,6 +410,9 @@ class EditValue{ } init(parent, name){ if(name){ + if(!parent){ + throw new Error("Parent is not defined") + } this.name = [parent, name] this.delete = !(name in parent) }else{ diff --git a/public/src/js/settings.js b/public/src/js/settings.js index 711a57b..38fa10a 100644 --- a/public/src/js/settings.js +++ b/public/src/js/settings.js @@ -253,27 +253,72 @@ class SettingsView{ } var settingBox = document.createElement("div") settingBox.classList.add("setting-box") + if(current.indent){ + settingBox.style.marginLeft = (2 * current.indent || 0).toString() + "em" + } var nameDiv = document.createElement("div") nameDiv.classList.add("setting-name", "stroke-sub") - var name = current.name || strings.settings[i].name + if(current.name || current.name_lang){ + var name = this.getLocalTitle(current.name, current.name_lang) + }else{ + var name = strings.settings[i].name + } this.setAltText(nameDiv, name) + if(current.description || current.description_lang){ + nameDiv.title = this.getLocalTitle(current.description, current.description_lang) || "" + } settingBox.appendChild(nameDiv) var valueDiv = document.createElement("div") valueDiv.classList.add("setting-value") - this.getValue(i, valueDiv) + let outputObject = { + id: i, + settingBox: settingBox, + nameDiv: nameDiv, + valueDiv: valueDiv, + name: current.name, + name_lang: current.name_lang, + description: current.description, + description_lang: current.description_lang + } + if(current.type === "number"){ + ["min", "max", "fixedPoint", "step", "sign", "format", "format_lang"].forEach(opt => { + if(opt in current){ + outputObject[opt] = current[opt] + } + }) + outputObject.valueText = document.createTextNode("") + valueDiv.appendChild(outputObject.valueText) + var buttons = document.createElement("div") + buttons.classList.add("latency-buttons") + var buttonMinus = document.createElement("span") + buttonMinus.innerText = "-" + buttons.appendChild(buttonMinus) + this.addTouchRepeat(buttonMinus, event => { + this.numberAdjust(outputObject, -1) + }) + var buttonPlus = document.createElement("span") + buttonPlus.innerText = "+" + buttons.appendChild(buttonPlus) + this.addTouchRepeat(buttonPlus, event => { + this.numberAdjust(outputObject, 1) + }) + valueDiv.appendChild(buttons) + this.addTouch(settingBox, event => { + if(event.target.tagName !== "SPAN"){ + this.setValue(i) + } + }) + }else{ + this.addTouchEnd(settingBox, event => this.setValue(i)) + } settingBox.appendChild(valueDiv) content.appendChild(settingBox) if(!toSetting && this.items.length === this.selected || toSetting === i){ this.selected = this.items.length settingBox.classList.add("selected") } - this.addTouchEnd(settingBox, event => this.setValue(i)) - this.items.push({ - id: i, - settingBox: settingBox, - nameDiv: nameDiv, - valueDiv: valueDiv - }) + this.items.push(outputObject) + this.getValue(i, valueDiv) } this.items.push({ id: "default", @@ -443,6 +488,9 @@ class SettingsView{ pageEvents.remove(element, ["mousedown", "touchend"]) } getValue(name, valueDiv){ + if(!this.items){ + return + } var current = this.settingsItems[name] if(current.getItem){ var value = current.getItem() @@ -482,6 +530,17 @@ class SettingsView{ } value += string }) + }else if(current.type === "number"){ + var mul = Math.pow(10, current.fixedPoint || 0) + this.items[name].value = value * mul + value = Intl.NumberFormat(strings.intl, current.sign ? { + signDisplay: "always" + } : undefined).format(value) + if(current.format || current.format_lang){ + value = this.getLocalTitle(current.format, current.format_lang).replace("%s", value) + } + this.items[name].valueText.data = value + return } valueDiv.innerText = value } @@ -496,6 +555,9 @@ class SettingsView{ var selectedIndex = this.items.findIndex(item => item.id === name) var selected = this.items[selectedIndex] if(this.mode !== "settings"){ + if(this.mode === "number"){ + return this.numberBack(this.items[this.selected]) + } if(this.selected === selectedIndex){ this.keyboardBack(selected) this.playSound("se_don") @@ -530,6 +592,12 @@ class SettingsView{ this.latencySet() this.playSound("se_don") return + }else if(current.type === "number"){ + this.mode = "number" + selected.settingBox.style.animation = "none" + selected.valueDiv.classList.add("selected") + this.playSound("se_don") + return } if(current.setItem){ promise = current.setItem(value) @@ -633,6 +701,19 @@ class SettingsView{ this.playSound(name === "confirm" ? "se_don" : "se_cancel") }else if(name === "up" || name === "right" || name === "down" || name === "left"){ this.latencySetAdjust(latencySelected, (name === "up" || name === "right") ? 1 : -1) + if(event){ + event.preventDefault() + } + } + }else if(this.mode === "number"){ + if(name === "confirm" || name === "back"){ + this.numberBack(selected) + this.playSound(name === "confirm" ? "se_don" : "se_cancel") + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + this.numberAdjust(selected, (name === "up" || name === "right") ? 1 : -1) + if(event){ + event.preventDefault() + } } } } @@ -833,6 +914,42 @@ class SettingsView{ this.latencySettings.style.display = "" this.mode = "settings" } + numberAdjust(selected, add){ + var selectedItem = this.items[this.selected] + var mul = Math.pow(10, selected.fixedPoint || 0) + selectedItem.value += add * ("step" in selected ? selected.step : 1) + if("max" in selected && selectedItem.value > selected.max * mul){ + selectedItem.value = selected.max * mul + }else if("min" in selected && selectedItem.value < selected.min * mul){ + selectedItem.value = selected.min * mul + }else{ + this.playSound("se_ka") + } + var valueText = Intl.NumberFormat(strings.intl, selected.sign ? { + signDisplay: "always" + } : undefined).format(selectedItem.value / mul) + if(selected.format || selected.format_lang){ + valueText = this.getLocalTitle(selected.format, selected.format_lang).replace("%s", valueText) + } + selectedItem.valueText.data = valueText + } + numberBack(selected){ + this.mode = "settings" + selected.settingBox.style.animation = "" + selected.valueDiv.classList.remove("selected") + var current = this.settingsItems[selected.id] + var promise + var mul = Math.pow(10, selected.fixedPoint || 0) + var value = selected.value / mul + if(current.setItem){ + promise = current.setItem(value) + }else{ + settings.setItem(selected.id, value) + } + (promise || Promise.resolve()).then(() => { + this.getValue(selected.id, selected.valueText) + }) + } addMs(input){ var split = strings.calibration.ms.split("%s") var index = 0 @@ -869,6 +986,9 @@ class SettingsView{ this.playSound("se_don") } onEnd(){ + if(this.mode === "number"){ + this.numberBack(this.items[this.selected]) + } this.clean() this.playSound("se_don") setTimeout(() => { @@ -882,6 +1002,16 @@ class SettingsView{ } }, 500) } + getLocalTitle(title, titleLang){ + if(titleLang){ + for(var id in titleLang){ + if(id === strings.id && titleLang[id]){ + return titleLang[id] + } + } + } + return title + } setLang(lang){ settings.setLang(lang) if(failedTests.length !== 0){ @@ -890,8 +1020,15 @@ class SettingsView{ for(var i in this.items){ var item = this.items[i] if(item.valueDiv){ - var name = strings.settings[item.id].name + if(item.name || item.name_lang){ + var name = this.getLocalTitle(item.name, item.name_lang) + }else{ + var name = strings.settings[item.id].name + } this.setAltText(item.nameDiv, name) + if(item.description || item.description_lang){ + item.nameDiv.title = this.getLocalTitle(item.description, item.description_lang) || "" + } this.getValue(item.id, item.valueDiv) } } diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 3545f45..4caebeb 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -1161,9 +1161,9 @@ class SongSelect{ selectedWidth = this.songAsset.selectedWidth } - var lastMoveMul = Math.pow(Math.abs(this.state.lastMove), 1 / 4) + var lastMoveMul = Math.pow(Math.abs(this.state.lastMove || 0), 1 / 4) var changeSpeed = this.songSelecting.speed * lastMoveMul - var resize = changeSpeed * this.songSelecting.resize / lastMoveMul + var resize = changeSpeed * (lastMoveMul === 0 ? 0 : this.songSelecting.resize / lastMoveMul) var scrollDelay = changeSpeed * this.songSelecting.scrollDelay var resize2 = changeSpeed - resize var scroll = resize2 - resize - scrollDelay * 2 diff --git a/public/src/js/strings.js b/public/src/js/strings.js index 25b0a4f..a7daf75 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -1323,6 +1323,14 @@ var translations = { one: "%s plugin", other: "%s plugins" } + }, + author: { + ja: null, + en: "By %s" + }, + version: { + ja: null, + en: "Version %s" } } } diff --git a/tools/hooks/post-checkout b/tools/hooks/post-checkout old mode 100644 new mode 100755 diff --git a/tools/hooks/post-commit b/tools/hooks/post-commit old mode 100644 new mode 100755 diff --git a/tools/hooks/post-merge b/tools/hooks/post-merge old mode 100644 new mode 100755 diff --git a/tools/hooks/post-rewrite b/tools/hooks/post-rewrite old mode 100644 new mode 100755