Plugins: Add plugin settings

- Add support for plugin settings, they appear in the same menu as the plugins, indented from the left to emphasize which plugin the setting belongs to
  - Note that plugin settings can still be changed even when the plugins are stopped
- Add tooltips to plugin menu to view the plugin descriptions, description_lang can also be used
- Fix scolling not working on song select when returning from game settings
- Let instance owners set default plugin files in config.py, to make them easier to maintain
- plugins.add() can now add plugins using a url
- Plugins can be hidden from the plugin menu using PluginLoader.hide, an option in plugins.add(), or in config.py
- Make p2.disable() incremental so that multiple plugins can disable multiplayer independently
- Server no longer crashes if certain optional config fields were not copied over from an updated example config
- Fix not being able to unload plugins if one was imported with errors
This commit is contained in:
KatieFrogs 2022-02-22 16:23:01 +03:00
parent 78fe7062dc
commit 7d818877f8
15 changed files with 375 additions and 71 deletions

59
app.py
View File

@ -3,7 +3,10 @@
import base64 import base64
import bcrypt import bcrypt
import hashlib 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 json
import re import re
import requests import requests
@ -20,23 +23,32 @@ from ffmpy import FFmpeg
from pymongo import MongoClient from pymongo import MongoClient
from redis import Redis from redis import Redis
app = Flask(__name__) def take_config(name, required=False):
client = MongoClient(host=config.MONGO['host']) 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' app.config['SESSION_TYPE'] = 'redis'
redis_config = take_config('REDIS', required=True)
app.config['SESSION_REDIS'] = Redis( app.config['SESSION_REDIS'] = Redis(
host=config.REDIS['CACHE_REDIS_HOST'], host=redis_config['CACHE_REDIS_HOST'],
port=config.REDIS['CACHE_REDIS_PORT'], port=redis_config['CACHE_REDIS_PORT'],
password=config.REDIS['CACHE_REDIS_PASSWORD'], password=redis_config['CACHE_REDIS_PASSWORD'],
db=config.REDIS['CACHE_REDIS_DB'] db=redis_config['CACHE_REDIS_DB']
) )
app.cache = Cache(app, config=config.REDIS) app.cache = Cache(app, config=redis_config)
sess = Session() sess = Session()
sess.init_app(app) sess.init_app(app)
csrf = CSRFProtect(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.users.create_index('username', unique=True)
db.songs.create_index('id', unique=True) db.songs.create_index('id', unique=True)
db.scores.create_index('username') db.scores.create_index('username')
@ -53,12 +65,12 @@ def api_error(message):
def generate_hash(id, form): def generate_hash(id, form):
md5 = hashlib.md5() md5 = hashlib.md5()
if form['type'] == 'tja': 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: else:
urls = [] urls = []
for diff in ['easy', 'normal', 'hard', 'oni', 'ura']: for diff in ['easy', 'normal', 'hard', 'oni', 'ura']:
if form['course_' + diff]: 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: for url in urls:
if url.startswith("http://") or url.startswith("https://"): if url.startswith("http://") or url.startswith("https://"):
@ -117,22 +129,24 @@ def before_request_func():
def get_config(credentials=False): def get_config(credentials=False):
config_out = { config_out = {
'songs_baseurl': config.SONGS_BASEURL, 'songs_baseurl': take_config('SONGS_BASEURL', required=True),
'assets_baseurl': config.ASSETS_BASEURL, 'assets_baseurl': take_config('ASSETS_BASEURL', required=True),
'email': config.EMAIL, 'email': take_config('EMAIL'),
'accounts': config.ACCOUNTS, 'accounts': take_config('ACCOUNTS'),
'custom_js': config.CUSTOM_JS, 'custom_js': take_config('CUSTOM_JS'),
'preview_type': config.PREVIEW_TYPE or 'mp3' '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: 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'): if not session.get('username'):
user_level = 0 user_level = 0
else: else:
user = db.users.find_one({'username': session.get('username')}) user = db.users.find_one({'username': session.get('username')})
user_level = user['user_level'] user_level = user['user_level']
if user_level >= min_level: if user_level >= min_level:
config_out['google_credentials'] = config.GOOGLE_CREDENTIALS config_out['google_credentials'] = google_credentials
else: else:
config_out['google_credentials'] = { config_out['google_credentials'] = {
'gdrive_enabled': False 'gdrive_enabled': False
@ -146,9 +160,8 @@ def get_config(credentials=False):
config_out['_version'] = get_version() config_out['_version'] = get_version()
return config_out return config_out
def get_version(): 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'): if os.path.isfile('version.json'):
try: try:
ver = json.load(open('version.json', 'r')) ver = json.load(open('version.json', 'r'))
@ -669,7 +682,7 @@ def route_api_scores_get():
@app.route('/privacy') @app.route('/privacy')
def route_api_privacy(): def route_api_privacy():
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt'))) 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 = make_response(render_template('privacy.txt', last_modified=last_modified, config=get_config(), integration=integration))
response.headers['Content-type'] = 'text/plain; charset=utf-8' response.headers['Content-type'] = 'text/plain; charset=utf-8'

View File

@ -13,6 +13,13 @@ ACCOUNTS = True
# Custom JavaScript file to load with the simulator. # Custom JavaScript file to load with the simulator.
CUSTOM_JS = '' CUSTOM_JS = ''
# Default plugins to load with the simulator.
PLUGINS = [{
'url': '',
'start': False,
'hide': False
}]
# Filetype to use for song previews. (mp3/ogg) # Filetype to use for song previews. (mp3/ogg)
PREVIEW_TYPE = 'mp3' PREVIEW_TYPE = 'mp3'

View File

@ -293,7 +293,7 @@ kbd{
#settings-latency .view{ #settings-latency .view{
width: 30em; width: 30em;
} }
#settings-latency .setting-value{ .setting-value{
position: relative; position: relative;
} }
.setting-value:not(.selected) .latency-buttons{ .setting-value:not(.selected) .latency-buttons{

View File

@ -56,6 +56,10 @@ function browserSupport(){
}, },
"KeyboardEvent.key": function(){ "KeyboardEvent.key": function(){
return "key" in KeyboardEvent.prototype return "key" in KeyboardEvent.prototype
},
"Module import": function(){
eval("import('data:text/javascript,')")
return true
} }
} }
failedTests = [] failedTests = []
@ -107,10 +111,12 @@ function showUnsupported(strings){
var warn = document.createElement("div") var warn = document.createElement("div")
warn.id = "unsupportedWarn" warn.id = "unsupportedWarn"
warn.innerText = "!" warn.innerText = "!"
warn.textContent = "!"
div.appendChild(warn) div.appendChild(warn)
var hide = document.createElement("div") var hide = document.createElement("div")
hide.id = "unsupportedHide" hide.id = "unsupportedHide"
hide.innerText = "x" hide.innerText = "x"
hide.textContent = "x"
div.appendChild(hide) div.appendChild(hide)
var span = document.createElement("span") var span = document.createElement("span")
@ -119,6 +125,7 @@ function showUnsupported(strings){
if(i !== 0){ if(i !== 0){
var link = document.createElement("a") var link = document.createElement("a")
link.innerText = strings.browserSupport.details link.innerText = strings.browserSupport.details
link.textContent = strings.browserSupport.details
span.appendChild(link) span.appendChild(link)
} }
span.appendChild(document.createTextNode(browserWarning[i])) span.appendChild(document.createTextNode(browserWarning[i]))
@ -133,6 +140,7 @@ function showUnsupported(strings){
for(var i = 0; i < failedTests.length; i++){ for(var i = 0; i < failedTests.length; i++){
var li = document.createElement("li") var li = document.createElement("li")
li.innerText = failedTests[i] li.innerText = failedTests[i]
li.textContent = failedTests[i]
ul.appendChild(li) ul.appendChild(li)
} }
details.appendChild(ul) details.appendChild(ul)
@ -143,6 +151,7 @@ function showUnsupported(strings){
var chrome = document.createElement("a") var chrome = document.createElement("a")
chrome.href = "https://www.google.com/chrome/" chrome.href = "https://www.google.com/chrome/"
chrome.innerText = "Google Chrome" chrome.innerText = "Google Chrome"
chrome.textContent = "Google Chrome"
details.appendChild(chrome) details.appendChild(chrome)
} }
details.appendChild(document.createTextNode(supportedBrowser[i])) details.appendChild(document.createTextNode(supportedBrowser[i]))

View File

@ -108,7 +108,10 @@
) )
))){ ))){
this.plugins.forEach(obj => { 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){ if(plugin){
pluginAmount++ pluginAmount++
plugin.imported = true plugin.imported = true

View File

@ -183,7 +183,7 @@ class Loader{
var image = document.createElement("img") var image = document.createElement("img")
var url = gameConfig.assets_baseurl + "img/" + name var url = gameConfig.assets_baseurl + "img/" + name
categoryPromises.push(pageEvents.load(image).catch(response => { categoryPromises.push(pageEvents.load(image).catch(response => {
this.errorMsg(response, url) return this.errorMsg(response, url)
})) }))
image.id = name image.id = name
image.src = url image.src = url
@ -325,6 +325,26 @@ class Loader{
promises.push(this.canvasTest.drawAllImages()) 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 => { Promise.all(promises).then(result => {
perf.allImg = result perf.allImg = result
perf.load = Date.now() - this.startTime perf.load = Date.now() - this.startTime
@ -332,8 +352,8 @@ class Loader{
this.clean() this.clean()
this.callback(songId) this.callback(songId)
pageEvents.send("ready", readyEvent) pageEvents.send("ready", readyEvent)
}) }, () => this.errorMsg())
}, this.errorMsg.bind(this)) }, () => this.errorMsg())
}) })
} }
addPromise(promise, url){ addPromise(promise, url){
@ -433,6 +453,7 @@ class Loader{
} }
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount)) 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```" this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```"
return Promise.reject(error)
} }
assetLoaded(){ assetLoaded(){
if(!this.error){ if(!this.error){

View File

@ -11,6 +11,7 @@ class P2Connection{
this.allEvents = new Map() this.allEvents = new Map()
this.addEventListener("message", this.message.bind(this)) this.addEventListener("message", this.message.bind(this))
this.currentHash = "" this.currentHash = ""
this.disabled = 0
pageEvents.add(window, "hashchange", this.onhashchange.bind(this)) pageEvents.add(window, "hashchange", this.onhashchange.bind(this))
} }
addEventListener(type, callback){ addEventListener(type, callback){
@ -257,11 +258,11 @@ class P2Connection{
} }
} }
enable(){ enable(){
this.disabled = false this.disabled = Math.max(0, this.disabled - 1)
this.open() setTimeout(this.open.bind(this), 100)
} }
disable(){ disable(){
this.disabled = true this.disabled++
this.close() this.close()
} }
} }

View File

@ -8,18 +8,39 @@ class Plugins{
this.hashes = [] this.hashes = []
this.startOrder = [] this.startOrder = []
} }
add(script, name){ add(script, options){
options = options || {}
var hash = md5.base64(script.toString()) 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){ if(this.hashes.indexOf(hash) !== -1){
console.warn("Skip adding an already addded plugin: " + name) console.warn("Skip adding an already addded plugin: " + name)
return return
} }
name = name || "plugin"
var baseName = name var baseName = name
for(var i = 2; name in this.pluginMap; i++){ for(var i = 2; name in this.pluginMap; i++){
name = baseName + i.toString() 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({ this.allPlugins.push({
name: name, name: name,
plugin: plugin plugin: plugin
@ -41,10 +62,14 @@ class Plugins{
if(index !== -1){ if(index !== -1){
this.allPlugins.splice(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] delete this.pluginMap[name]
} }
load(name){ load(name){
this.pluginMap[name].load() return this.pluginMap[name].load()
} }
loadAll(){ loadAll(){
for(var i = 0; i < this.allPlugins.length; i++){ for(var i = 0; i < this.allPlugins.length; i++){
@ -52,7 +77,7 @@ class Plugins{
} }
} }
start(name){ start(name){
this.pluginMap[name].start() return this.pluginMap[name].start()
} }
startAll(){ startAll(){
for(var i = 0; i < this.allPlugins.length; i++){ for(var i = 0; i < this.allPlugins.length; i++){
@ -60,7 +85,7 @@ class Plugins{
} }
} }
stop(name){ stop(name){
this.pluginMap[name].stop() return this.pluginMap[name].stop()
} }
stopAll(){ stopAll(){
for(var i = this.startOrder.length; i--;){ for(var i = this.startOrder.length; i--;){
@ -68,7 +93,7 @@ class Plugins{
} }
} }
unload(name){ unload(name){
this.pluginMap[name].unload() return this.pluginMap[name].unload()
} }
unloadAll(){ unloadAll(){
for(var i = this.startOrder.length; i--;){ for(var i = this.startOrder.length; i--;){
@ -127,30 +152,79 @@ class Plugins{
} }
getSettings(){ getSettings(){
var items = {} var items = []
for(var i = 0; i < this.allPlugins.length; i++){ for(var i = 0; i < this.allPlugins.length; i++){
var obj = this.allPlugins[i] var obj = this.allPlugins[i]
let plugin = obj.plugin let plugin = obj.plugin
items[obj.name] = { if(!plugin.loaded){
name: plugin.module ? this.getLocalTitle(plugin.module.name || obj.name, plugin.module.name_lang) : obj.name, continue
type: "toggle", }
default: true, if(!plugin.hide){
getItem: () => plugin.started, let description
setItem: value => { let description_lang
if(plugin.started && !value){ var module = plugin.module
this.stop(plugin.name) if(module){
}else if(!plugin.started && value){ description = [
this.start(plugin.name) 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 return items
} }
getLocalTitle(title, titleLang){ getLocalTitle(title, titleLang, lang){
if(titleLang){ if(titleLang){
for(var id in titleLang){ for(var id in titleLang){
if(id === strings.id && titleLang[id]){ if(id === (lang || strings.id) && titleLang[id]){
return titleLang[id] return titleLang[id]
} }
} }
@ -163,20 +237,30 @@ class PluginLoader{
constructor(...args){ constructor(...args){
this.init(...args) this.init(...args)
} }
init(script, name, hash){ init(script, name, hash, raw){
this.name = name this.name = name
this.hash = hash this.hash = hash
if(typeof script === "string"){ if(typeof script === "string"){
this.url = URL.createObjectURL(new Blob([script], { if(raw){
type: "application/javascript" this.url = URL.createObjectURL(new Blob([script], {
})) type: "application/javascript"
}))
}else{
this.url = script
}
}else{ }else{
this.class = script this.class = script
} }
} }
load(){ load(loadErrors){
if(this.loaded || !this.url && !this.class){ if(this.loaded){
return Promise.resolve() return Promise.resolve()
}else if(!this.url && !this.class){
if(loadErrors){
return Promise.reject()
}else{
return Promise.resolve()
}
}else{ }else{
return (this.url ? import(this.url) : Promise.resolve({ return (this.url ? import(this.url) : Promise.resolve({
default: this.class default: this.class
@ -209,7 +293,11 @@ class PluginLoader{
}, e => { }, e => {
console.error(e) console.error(e)
this.error() this.error()
return Promise.resolve() if(loadErrors){
return Promise.reject(e)
}else{
return Promise.resolve()
}
}) })
} }
} }
@ -300,6 +388,20 @@ class PluginLoader{
} }
this.unload(true) 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{ class EditValue{
@ -308,6 +410,9 @@ class EditValue{
} }
init(parent, name){ init(parent, name){
if(name){ if(name){
if(!parent){
throw new Error("Parent is not defined")
}
this.name = [parent, name] this.name = [parent, name]
this.delete = !(name in parent) this.delete = !(name in parent)
}else{ }else{

View File

@ -253,27 +253,72 @@ class SettingsView{
} }
var settingBox = document.createElement("div") var settingBox = document.createElement("div")
settingBox.classList.add("setting-box") settingBox.classList.add("setting-box")
if(current.indent){
settingBox.style.marginLeft = (2 * current.indent || 0).toString() + "em"
}
var nameDiv = document.createElement("div") var nameDiv = document.createElement("div")
nameDiv.classList.add("setting-name", "stroke-sub") 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) this.setAltText(nameDiv, name)
if(current.description || current.description_lang){
nameDiv.title = this.getLocalTitle(current.description, current.description_lang) || ""
}
settingBox.appendChild(nameDiv) settingBox.appendChild(nameDiv)
var valueDiv = document.createElement("div") var valueDiv = document.createElement("div")
valueDiv.classList.add("setting-value") 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) settingBox.appendChild(valueDiv)
content.appendChild(settingBox) content.appendChild(settingBox)
if(!toSetting && this.items.length === this.selected || toSetting === i){ if(!toSetting && this.items.length === this.selected || toSetting === i){
this.selected = this.items.length this.selected = this.items.length
settingBox.classList.add("selected") settingBox.classList.add("selected")
} }
this.addTouchEnd(settingBox, event => this.setValue(i)) this.items.push(outputObject)
this.items.push({ this.getValue(i, valueDiv)
id: i,
settingBox: settingBox,
nameDiv: nameDiv,
valueDiv: valueDiv
})
} }
this.items.push({ this.items.push({
id: "default", id: "default",
@ -443,6 +488,9 @@ class SettingsView{
pageEvents.remove(element, ["mousedown", "touchend"]) pageEvents.remove(element, ["mousedown", "touchend"])
} }
getValue(name, valueDiv){ getValue(name, valueDiv){
if(!this.items){
return
}
var current = this.settingsItems[name] var current = this.settingsItems[name]
if(current.getItem){ if(current.getItem){
var value = current.getItem() var value = current.getItem()
@ -482,6 +530,17 @@ class SettingsView{
} }
value += string 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 valueDiv.innerText = value
} }
@ -496,6 +555,9 @@ class SettingsView{
var selectedIndex = this.items.findIndex(item => item.id === name) var selectedIndex = this.items.findIndex(item => item.id === name)
var selected = this.items[selectedIndex] var selected = this.items[selectedIndex]
if(this.mode !== "settings"){ if(this.mode !== "settings"){
if(this.mode === "number"){
return this.numberBack(this.items[this.selected])
}
if(this.selected === selectedIndex){ if(this.selected === selectedIndex){
this.keyboardBack(selected) this.keyboardBack(selected)
this.playSound("se_don") this.playSound("se_don")
@ -530,6 +592,12 @@ class SettingsView{
this.latencySet() this.latencySet()
this.playSound("se_don") this.playSound("se_don")
return 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){ if(current.setItem){
promise = current.setItem(value) promise = current.setItem(value)
@ -633,6 +701,19 @@ class SettingsView{
this.playSound(name === "confirm" ? "se_don" : "se_cancel") this.playSound(name === "confirm" ? "se_don" : "se_cancel")
}else if(name === "up" || name === "right" || name === "down" || name === "left"){ }else if(name === "up" || name === "right" || name === "down" || name === "left"){
this.latencySetAdjust(latencySelected, (name === "up" || name === "right") ? 1 : -1) 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.latencySettings.style.display = ""
this.mode = "settings" 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){ addMs(input){
var split = strings.calibration.ms.split("%s") var split = strings.calibration.ms.split("%s")
var index = 0 var index = 0
@ -869,6 +986,9 @@ class SettingsView{
this.playSound("se_don") this.playSound("se_don")
} }
onEnd(){ onEnd(){
if(this.mode === "number"){
this.numberBack(this.items[this.selected])
}
this.clean() this.clean()
this.playSound("se_don") this.playSound("se_don")
setTimeout(() => { setTimeout(() => {
@ -882,6 +1002,16 @@ class SettingsView{
} }
}, 500) }, 500)
} }
getLocalTitle(title, titleLang){
if(titleLang){
for(var id in titleLang){
if(id === strings.id && titleLang[id]){
return titleLang[id]
}
}
}
return title
}
setLang(lang){ setLang(lang){
settings.setLang(lang) settings.setLang(lang)
if(failedTests.length !== 0){ if(failedTests.length !== 0){
@ -890,8 +1020,15 @@ class SettingsView{
for(var i in this.items){ for(var i in this.items){
var item = this.items[i] var item = this.items[i]
if(item.valueDiv){ 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) 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) this.getValue(item.id, item.valueDiv)
} }
} }

View File

@ -1161,9 +1161,9 @@ class SongSelect{
selectedWidth = this.songAsset.selectedWidth 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 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 scrollDelay = changeSpeed * this.songSelecting.scrollDelay
var resize2 = changeSpeed - resize var resize2 = changeSpeed - resize
var scroll = resize2 - resize - scrollDelay * 2 var scroll = resize2 - resize - scrollDelay * 2

View File

@ -1323,6 +1323,14 @@ var translations = {
one: "%s plugin", one: "%s plugin",
other: "%s plugins" other: "%s plugins"
} }
},
author: {
ja: null,
en: "By %s"
},
version: {
ja: null,
en: "Version %s"
} }
} }
} }

0
tools/hooks/post-checkout Normal file → Executable file
View File

0
tools/hooks/post-commit Normal file → Executable file
View File

0
tools/hooks/post-merge Normal file → Executable file
View File

0
tools/hooks/post-rewrite Normal file → Executable file
View File