Compare commits
43 Commits
bootstrap-
...
master
Author | SHA1 | Date |
---|---|---|
rubenwardy | 8ad066409c | |
rubenwardy | 4ac8949c3a | |
rubenwardy | 83b2cf48d4 | |
rubenwardy | 2bbb117eac | |
rubenwardy | f61112a8d7 | |
rubenwardy | 3566b030c5 | |
rubenwardy | 2d54fe4ed7 | |
rubenwardy | 7fdd2cc7c9 | |
rubenwardy | 81a85cbbe5 | |
rubenwardy | 4902436b6b | |
rubenwardy | b82bcb0af9 | |
rubenwardy | eeea5d004a | |
rubenwardy | 97ee0a9f85 | |
rubenwardy | 958f92fd63 | |
rubenwardy | dfef268b05 | |
rubenwardy | e7d2f09eb4 | |
rubenwardy | 5bb9012655 | |
rubenwardy | a291b2cd6f | |
rubenwardy | ead077fb92 | |
rubenwardy | 1c9d6ac865 | |
rubenwardy | d098ee9dff | |
rubenwardy | b8d95dd222 | |
rubenwardy | 7c93db95a3 | |
rubenwardy | d529634b7f | |
Y.W | 765b5603c1 | |
Gao Tiesuan | eec39a3fc5 | |
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi | 72f66530aa | |
Nikita Epifanov | 99ee1cfc7e | |
rubenwardy | f8e82b63e3 | |
rubenwardy | afdf06b3f6 | |
rubenwardy | d21a86587f | |
rubenwardy | 38071165d1 | |
rubenwardy | 1cfc152d3b | |
rubenwardy | 2db2f61992 | |
Balázs Kovács | 4543f6ca39 | |
Nikita Epifanov | f8d518300d | |
Andrij Mizyk | 347e214944 | |
Mikitko | 99b4d8e084 | |
Nikita Epifanov | 313cab6b2d | |
debiankaios | 494559cfd7 | |
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi | e3326aa0f1 | |
rubenwardy | bdd3ab4360 | |
rubenwardy | 4f9ec2e8a4 |
|
@ -4,7 +4,9 @@
|
|||
Content database for Minetest mods, games, and more.\
|
||||
Developed by rubenwardy, license AGPLv3.0+.
|
||||
|
||||
See [Getting Started](docs/getting_started.md).
|
||||
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment.
|
||||
|
||||
See [Developer Intro](docs/dev_intro.md) for an overview of the code organisation.
|
||||
|
||||
## How-tos
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ app.config["LANGUAGES"] = {
|
|||
"fr": "Français",
|
||||
"id": "Bahasa Indonesia",
|
||||
"ms": "Bahasa Melayu",
|
||||
"ru": "русский язык",
|
||||
}
|
||||
|
||||
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
|
||||
|
|
|
@ -16,15 +16,18 @@
|
|||
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
from celery import group
|
||||
from flask import redirect, url_for, flash, current_app
|
||||
from flask import redirect, url_for, flash, current_app, jsonify
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
from app.logic.game_support import GameSupportResolver
|
||||
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
|
||||
NotificationType, PackageUpdateConfig, License, UserRank, PackageType
|
||||
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, PackageGameSupport
|
||||
from app.tasks.emails import send_pending_digests
|
||||
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
|
||||
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
|
||||
from app.utils import addNotification, get_system_user
|
||||
|
@ -321,3 +324,15 @@ def update_screenshot_sizes():
|
|||
screenshot.height = height
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@action("Detect game support")
|
||||
def detect_game_support():
|
||||
resolver = GameSupportResolver()
|
||||
resolver.update_all()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@action("Send pending notif digests")
|
||||
def do_send_pending_digests():
|
||||
send_pending_digests.delay()
|
||||
|
|
|
@ -39,7 +39,7 @@ def audit():
|
|||
return render_template("admin/audit.html", log=pagination.items, pagination=pagination)
|
||||
|
||||
|
||||
@bp.route("/admin/audit/<int:id>/")
|
||||
@bp.route("/admin/audit/<int:id_>/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def audit_view(id_):
|
||||
entry = AuditLogEntry.query.get(id_)
|
||||
|
|
|
@ -13,13 +13,14 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
import flask_sqlalchemy
|
||||
from flask import request, jsonify, current_app
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.orm import subqueryload, joinedload
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql.expression import func
|
||||
|
||||
from app import csrf
|
||||
|
@ -30,7 +31,8 @@ from app.querybuilder import QueryBuilder
|
|||
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes
|
||||
from . import bp
|
||||
from .auth import is_api_authd
|
||||
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, api_order_screenshots, api_edit_package
|
||||
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
|
||||
api_order_screenshots, api_edit_package, api_set_cover_image
|
||||
from functools import wraps
|
||||
|
||||
|
||||
|
@ -302,7 +304,7 @@ def create_screenshot(token: APIToken, package: Package):
|
|||
if file is None:
|
||||
error(400, "Missing 'file' in multipart body")
|
||||
|
||||
return api_create_screenshot(token, package, data["title"], file)
|
||||
return api_create_screenshot(token, package, data["title"], file, isYes(data.get("is_cover_image")))
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/")
|
||||
|
@ -355,7 +357,7 @@ def order_screenshots(token: APIToken, package: Package):
|
|||
error(401, "Authentication needed")
|
||||
|
||||
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
|
||||
error(403, "You do not have the permission to delete screenshots")
|
||||
error(403, "You do not have the permission to change screenshots")
|
||||
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
@ -367,6 +369,28 @@ def order_screenshots(token: APIToken, package: Package):
|
|||
return api_order_screenshots(token, package, request.json)
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/screenshots/cover-image/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
@is_package_page
|
||||
@is_api_authd
|
||||
@cors_allowed
|
||||
def set_cover_image(token: APIToken, package: Package):
|
||||
if not token:
|
||||
error(401, "Authentication needed")
|
||||
|
||||
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
|
||||
error(403, "You do not have the permission to change screenshots")
|
||||
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
json = request.json
|
||||
if json is None or not isinstance(json, dict) or "cover_image" not in json:
|
||||
error(400, "Expected body to be an object with cover_image as a key")
|
||||
|
||||
return api_set_cover_image(token, package, request.json["cover_image"])
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/reviews/")
|
||||
@is_package_page
|
||||
@cors_allowed
|
||||
|
@ -477,6 +501,26 @@ def homepage():
|
|||
})
|
||||
|
||||
|
||||
@bp.route("/api/welcome/v1/")
|
||||
@cors_allowed
|
||||
def welcome_v1():
|
||||
featured = Package.query \
|
||||
.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED,
|
||||
Package.tags.any(name="featured")) \
|
||||
.order_by(func.random()) \
|
||||
.limit(5).all()
|
||||
|
||||
mtg = Package.query.filter(Package.author.has(username="Minetest"), Package.name == "minetest_game").one()
|
||||
featured.insert(2, mtg)
|
||||
|
||||
def map_packages(packages: List[Package]):
|
||||
return [pkg.getAsDictionaryShort(current_app.config["BASE_URL"]) for pkg in packages]
|
||||
|
||||
return jsonify({
|
||||
"featured": map_packages(featured),
|
||||
})
|
||||
|
||||
|
||||
@bp.route("/api/minetest_versions/")
|
||||
@cors_allowed
|
||||
def versions():
|
||||
|
|
|
@ -19,7 +19,7 @@ from flask import jsonify, abort, make_response, url_for, current_app
|
|||
|
||||
from app.logic.packages import do_edit_package
|
||||
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
|
||||
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
|
||||
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
|
||||
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot
|
||||
|
||||
|
||||
|
@ -69,13 +69,13 @@ def api_create_zip_release(token: APIToken, package: Package, title: str, file,
|
|||
})
|
||||
|
||||
|
||||
def api_create_screenshot(token: APIToken, package: Package, title: str, file, reason="API"):
|
||||
def api_create_screenshot(token: APIToken, package: Package, title: str, file, is_cover_image: bool, reason="API"):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
reason += ", token=" + token.name
|
||||
|
||||
ss : PackageScreenshot = guard(do_create_screenshot)(token.owner, package, title, file, reason)
|
||||
ss : PackageScreenshot = guard(do_create_screenshot)(token.owner, package, title, file, is_cover_image, reason)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
@ -94,6 +94,17 @@ def api_order_screenshots(token: APIToken, package: Package, order: [any]):
|
|||
})
|
||||
|
||||
|
||||
def api_set_cover_image(token: APIToken, package: Package, cover_image):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
guard(do_set_cover_image)(token.owner, package, cover_image)
|
||||
|
||||
return jsonify({
|
||||
"success": True
|
||||
})
|
||||
|
||||
|
||||
def api_edit_package(token: APIToken, package: Package, data: dict, reason: str = "API"):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
|
|
@ -53,12 +53,11 @@ def view(name):
|
|||
.filter(Dependency.optional==True, Package.state==PackageState.APPROVED) \
|
||||
.all()
|
||||
|
||||
similar_topics = None
|
||||
if mpackage.packages.filter_by(state=PackageState.APPROVED).count() == 0:
|
||||
similar_topics = ForumTopic.query \
|
||||
.filter_by(name=name) \
|
||||
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||
.all()
|
||||
similar_topics = ForumTopic.query \
|
||||
.filter_by(name=name) \
|
||||
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
|
||||
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||
.all()
|
||||
|
||||
return render_template("metapackages/view.html", mpackage=mpackage,
|
||||
dependers=dependers, optional_dependers=optional_dependers,
|
||||
|
|
|
@ -65,4 +65,4 @@ def get_package_tabs(user: User, package: Package):
|
|||
]
|
||||
|
||||
|
||||
from . import packages, screenshots, releases, reviews
|
||||
from . import packages, screenshots, releases, reviews, game_hub
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# ContentDB
|
||||
# Copyright (C) 2022 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import render_template, abort
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from . import bp
|
||||
from app.utils import is_package_page
|
||||
from ...models import Package, PackageType, PackageState, db, PackageRelease
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/hub/")
|
||||
@is_package_page
|
||||
def game_hub(package: Package):
|
||||
if package.type != PackageType.GAME:
|
||||
abort(404)
|
||||
|
||||
def join(query):
|
||||
return query.options(
|
||||
joinedload(Package.license),
|
||||
joinedload(Package.media_license))
|
||||
|
||||
query = Package.query.filter(Package.supported_games.any(game=package), Package.state==PackageState.APPROVED)
|
||||
count = query.count()
|
||||
|
||||
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
|
||||
pop_mod = join(query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score))).limit(8).all()
|
||||
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all()
|
||||
pop_txp = join(query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score))).limit(8).all()
|
||||
high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
|
||||
.filter(Package.reviews.any()).limit(4).all()
|
||||
|
||||
updated = db.session.query(Package).select_from(PackageRelease).join(Package) \
|
||||
.filter(Package.supported_games.any(game=package), Package.state==PackageState.APPROVED) \
|
||||
.order_by(db.desc(PackageRelease.releaseDate)) \
|
||||
.limit(20).all()
|
||||
updated = updated[:4]
|
||||
|
||||
return render_template("packages/game_hub.html", package=package, count=count,
|
||||
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam,
|
||||
high_reviewed=high_reviewed)
|
|
@ -115,9 +115,6 @@ def getReleases(package):
|
|||
@bp.route("/packages/<author>/<name>/")
|
||||
@is_package_page
|
||||
def view(package):
|
||||
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
|
||||
abort(404)
|
||||
|
||||
show_similar = not package.approved and (
|
||||
current_user in package.maintainers or
|
||||
package.checkPerm(current_user, Permission.APPROVE_NEW))
|
||||
|
@ -208,9 +205,6 @@ def shield(package, type):
|
|||
@bp.route("/packages/<author>/<name>/download/")
|
||||
@is_package_page
|
||||
def download(package):
|
||||
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
|
||||
abort(404)
|
||||
|
||||
release = package.getDownloadRelease()
|
||||
|
||||
if release is None:
|
||||
|
@ -593,9 +587,6 @@ def alias_create_edit(package: Package, alias_id: int = None):
|
|||
@login_required
|
||||
@is_package_page
|
||||
def share(package):
|
||||
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
|
||||
abort(404)
|
||||
|
||||
return render_template("packages/share.html", package=package,
|
||||
tabs=get_package_tabs(current_user, package), current_tab="share")
|
||||
|
||||
|
@ -603,9 +594,6 @@ def share(package):
|
|||
@bp.route("/packages/<author>/<name>/similar/")
|
||||
@is_package_page
|
||||
def similar(package):
|
||||
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
|
||||
abort(404)
|
||||
|
||||
packages_modnames = {}
|
||||
for metapackage in package.provides:
|
||||
packages_modnames[metapackage] = Package.query.filter(Package.id != package.id,
|
||||
|
|
|
@ -33,9 +33,6 @@ from . import bp, get_package_tabs
|
|||
@bp.route("/packages/<author>/<name>/releases/", methods=["GET", "POST"])
|
||||
@is_package_page
|
||||
def list_releases(package):
|
||||
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
|
||||
abort(404)
|
||||
|
||||
return render_template("packages/releases_list.html",
|
||||
package=package,
|
||||
tabs=get_package_tabs(current_user, package), current_tab="releases")
|
||||
|
@ -111,9 +108,6 @@ def create_release(package):
|
|||
@bp.route("/packages/<author>/<name>/releases/<id>/download/")
|
||||
@is_package_page
|
||||
def download_release(package, id):
|
||||
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
|
||||
abort(404)
|
||||
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
abort(404)
|
||||
|
|
|
@ -25,8 +25,8 @@ from flask_wtf import FlaskForm
|
|||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \
|
||||
Permission
|
||||
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required
|
||||
Permission, AuditSeverity
|
||||
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog
|
||||
from app.tasks.webhooktasks import post_discord_webhook
|
||||
|
||||
|
||||
|
@ -54,9 +54,6 @@ def review(package):
|
|||
flash(gettext("You can't review your own package!"), "danger")
|
||||
return redirect(package.getURL("packages.view"))
|
||||
|
||||
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
|
||||
abort(404)
|
||||
|
||||
review = PackageReview.query.filter_by(package=package, author=current_user).first()
|
||||
|
||||
form = ReviewForm(formdata=request.form, obj=review)
|
||||
|
@ -129,14 +126,19 @@ def review(package):
|
|||
form=form, package=package, review=review)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/review/delete/", methods=["POST"])
|
||||
@bp.route("/packages/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def delete_review(package):
|
||||
review = PackageReview.query.filter_by(package=package, author=current_user).first()
|
||||
def delete_review(package, reviewer):
|
||||
review = PackageReview.query \
|
||||
.filter(PackageReview.package == package, PackageReview.author.has(username=reviewer)) \
|
||||
.first()
|
||||
if review is None or review.package != package:
|
||||
abort(404)
|
||||
|
||||
if not review.checkPerm(current_user, Permission.DELETE_REVIEW):
|
||||
abort(403)
|
||||
|
||||
thread = review.thread
|
||||
|
||||
reply = ThreadReply()
|
||||
|
@ -147,10 +149,17 @@ def delete_review(package):
|
|||
|
||||
thread.review = None
|
||||
|
||||
msg = "Converted review by {} to thread".format(review.author.display_name)
|
||||
addAuditLog(AuditSeverity.MODERATION if current_user.username != reviewer else AuditSeverity.NORMAL,
|
||||
current_user, msg, thread.getViewURL(), thread.package)
|
||||
|
||||
notif_msg = "Deleted review '{}', comments were kept as a thread".format(thread.title)
|
||||
addNotification(package.maintainers, current_user, NotificationType.OTHER, notif_msg, url_for("threads.view", id=thread.id), package)
|
||||
|
||||
db.session.delete(review)
|
||||
|
||||
package.recalcScore()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(thread.getViewURL())
|
||||
|
@ -228,4 +237,4 @@ def review_votes(package):
|
|||
user_biases_info.sort(key=lambda x: -abs(x.balance))
|
||||
|
||||
return render_template("packages/review_votes.html", form=form, package=package, reviews=package.reviews,
|
||||
user_biases=user_biases_info)
|
||||
user_biases=user_biases_info)
|
||||
|
|
|
@ -87,7 +87,7 @@ def create_screenshot(package):
|
|||
form = CreateScreenshotForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
do_create_screenshot(current_user, package, form.title.data, form.fileUpload.data)
|
||||
do_create_screenshot(current_user, package, form.title.data, form.fileUpload.data, False)
|
||||
return redirect(package.getURL("packages.screenshots"))
|
||||
except LogicError as e:
|
||||
flash(e.message, "danger")
|
||||
|
|
|
@ -185,8 +185,8 @@ def view_user(username=None):
|
|||
.all()
|
||||
|
||||
needs_tags = user.maintained_packages \
|
||||
.filter(Package.state != PackageState.DELETED) \
|
||||
.filter_by(tags=None).order_by(db.asc(Package.title)).all()
|
||||
.filter(Package.state != PackageState.DELETED, Package.tags==None) \
|
||||
.order_by(db.asc(Package.title)).all()
|
||||
|
||||
return render_template("todo/user.html", current_tab="user", user=user,
|
||||
unapproved_packages=unapproved_packages, outdated_packages=outdated_packages,
|
||||
|
|
|
@ -90,6 +90,7 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
|
|||
* `issue_tracker`: Issue tracker URL.
|
||||
* `forums`: forum topic ID.
|
||||
* `video_url`: URL to a video.
|
||||
* `game_support`: Array of game support information objects. Not currently documented, as subject to change.
|
||||
* GET `/api/packages/<username>/<name>/dependencies/`
|
||||
* Returns dependencies, with suggested candidates
|
||||
* If query argument `only_hard` is present, only hard deps will be returned.
|
||||
|
@ -225,6 +226,7 @@ curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/
|
|||
* `url`: absolute URL to screenshot.
|
||||
* `created_at`: ISO time.
|
||||
* `order`: Number used in ordering.
|
||||
* `is_cover_image`: true for cover image.
|
||||
* GET `/api/packages/<username>/<name>/screenshots/<id>/` (Read)
|
||||
* Returns screenshot dictionary like above.
|
||||
* POST `/api/packages/<username>/<name>/screenshots/new/` (Create)
|
||||
|
@ -232,12 +234,16 @@ curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/
|
|||
* Body is multipart form data.
|
||||
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
|
||||
* `file`: multipart file to upload, like `<input type=file>`.
|
||||
* `is_cover_image`: set cover image to this.
|
||||
* DELETE `/api/packages/<username>/<name>/screenshots/<id>/` (Delete)
|
||||
* Requires authentication.
|
||||
* Deletes screenshot.
|
||||
* POST `/api/packages/<username>/<name>/screenshots/order/`
|
||||
* Requires authentication.
|
||||
* Body is a JSON array containing the screenshot IDs in their order.
|
||||
* POST `/api/packages/<username>/<name>/screenshots/cover-image/`
|
||||
* Requires authentication.
|
||||
* Body is a JSON dictionary with "cover_image" containing the screenshot ID.
|
||||
|
||||
Currently, to get a different size of thumbnail you can replace the number in `/thumbnails/1/` with any number from 1-3.
|
||||
The resolutions returned may change in the future, and we may move to a more capable thumbnail generation.
|
||||
|
@ -249,6 +255,11 @@ Examples:
|
|||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F file=@path/to/screnshot.png
|
||||
|
||||
# Create screenshot and set it as the cover image
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F file=@path/to/screnshot.png -F is_cover_image="true"
|
||||
|
||||
# Delete screenshot
|
||||
curl -X DELETE https://content.minetest.net/api/packages/username/name/screenshots/3/ \
|
||||
|
@ -258,6 +269,11 @@ curl -X DELETE https://content.minetest.net/api/packages/username/name/screensho
|
|||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/order/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d "[13, 2, 5, 7]"
|
||||
|
||||
# Set cover image
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/cover-image/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d "{ 'cover_image': 123 }"
|
||||
```
|
||||
|
||||
|
||||
|
@ -330,9 +346,11 @@ Supported query parameters:
|
|||
### Tags
|
||||
|
||||
* GET `/api/tags/` ([View](/api/tags/)): List of:
|
||||
* `name`: technical name
|
||||
* `title`: human-readable title
|
||||
* `description`: tag description or null
|
||||
* `name`: technical name.
|
||||
* `title`: human-readable title.
|
||||
* `description`: tag description or null.
|
||||
* `is_protected`: boolean, whether the tag is protected (can only be set by Editors in the web interface).
|
||||
* `views`: number of views of this tag.
|
||||
|
||||
### Content Warnings
|
||||
|
||||
|
@ -376,3 +394,5 @@ Supported query parameters:
|
|||
* `pop_txp`: popular textures
|
||||
* `pop_game`: popular games
|
||||
* `high_reviewed`: highest reviewed
|
||||
* GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning)
|
||||
* `featured`: featured games
|
||||
|
|
|
@ -25,8 +25,8 @@ A flag can be:
|
|||
There are also two meta-flags, which are designed so that we can change how different platforms filter the package list
|
||||
without making a release.
|
||||
|
||||
* `android_default`: currently same as `*, deprecated`. Hides all content warnings, WIP packages, and deprecated packages
|
||||
* `desktop_default`: currently same as `deprecated`. Hides all WIP and deprecated packages
|
||||
* `android_default`: currently same as `*, deprecated`. Hides all content warnings and deprecated packages
|
||||
* `desktop_default`: currently same as `deprecated`. Hides deprecated packages
|
||||
|
||||
## Content Warnings
|
||||
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
# ContentDB
|
||||
# Copyright (C) 2022 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from typing import List, Dict, Optional, Iterator, Iterable
|
||||
|
||||
from app.logic.LogicError import LogicError
|
||||
from app.models import Package, MetaPackage, PackageType, PackageState, PackageGameSupport, db
|
||||
|
||||
"""
|
||||
get_game_support(package):
|
||||
if package is a game:
|
||||
return [ package ]
|
||||
|
||||
for all hard dependencies:
|
||||
support = support AND get_meta_package_support(dep)
|
||||
|
||||
return support
|
||||
|
||||
get_meta_package_support(meta):
|
||||
for package implementing meta package:
|
||||
support = support OR get_game_support(package)
|
||||
|
||||
return support
|
||||
"""
|
||||
|
||||
|
||||
minetest_game_mods = {
|
||||
"beds", "boats", "bucket", "carts", "default", "dungeon_loot", "env_sounds", "fire", "flowers",
|
||||
"give_initial_stuff", "map", "player_api", "sethome", "spawn", "tnt", "walls", "wool",
|
||||
"binoculars", "bones", "butterflies", "creative", "doors", "dye", "farming", "fireflies", "game_commands",
|
||||
"keys", "mtg_craftguide", "screwdriver", "sfinv", "stairs", "vessels", "weather", "xpanes",
|
||||
}
|
||||
|
||||
|
||||
mtg_mod_blacklist = {
|
||||
"repixture", "tutorial", "runorfall", "realtest_mt5", "mevo", "xaenvironment",
|
||||
"survivethedays"
|
||||
}
|
||||
|
||||
|
||||
class PackageSet:
|
||||
packages: Dict[str, Package]
|
||||
|
||||
def __init__(self, packages: Optional[Iterable[Package]] = None):
|
||||
self.packages = {}
|
||||
if packages:
|
||||
self.update(packages)
|
||||
|
||||
def update(self, packages: Iterable[Package]):
|
||||
for package in packages:
|
||||
key = package.getId()
|
||||
if key not in self.packages:
|
||||
self.packages[key] = package
|
||||
|
||||
def intersection_update(self, other):
|
||||
keys = set(self.packages.keys())
|
||||
keys.difference_update(set(other.packages.keys()))
|
||||
for key in keys:
|
||||
del self.packages[key]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.packages)
|
||||
|
||||
def __iter__(self):
|
||||
return self.packages.values().__iter__()
|
||||
|
||||
|
||||
class GameSupportResolver:
|
||||
checked_packages = set()
|
||||
checked_metapackages = set()
|
||||
resolved_packages: Dict[str, PackageSet] = {}
|
||||
resolved_metapackages: Dict[str, PackageSet] = {}
|
||||
|
||||
def resolve_for_meta_package(self, meta: MetaPackage, history: List[str]) -> PackageSet:
|
||||
print(f"Resolving for {meta.name}", file=sys.stderr)
|
||||
|
||||
key = meta.name
|
||||
if key in self.resolved_metapackages:
|
||||
return self.resolved_metapackages.get(key)
|
||||
|
||||
if key in self.checked_metapackages:
|
||||
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
|
||||
return PackageSet()
|
||||
|
||||
self.checked_metapackages.add(key)
|
||||
|
||||
retval = PackageSet()
|
||||
|
||||
for package in meta.packages:
|
||||
if package.state != PackageState.APPROVED:
|
||||
continue
|
||||
|
||||
if meta.name in minetest_game_mods and package.name in mtg_mod_blacklist:
|
||||
continue
|
||||
|
||||
ret = self.resolve(package, history)
|
||||
if len(ret) == 0:
|
||||
retval = PackageSet()
|
||||
break
|
||||
|
||||
retval.update(ret)
|
||||
|
||||
self.resolved_metapackages[key] = retval
|
||||
return retval
|
||||
|
||||
def resolve(self, package: Package, history: List[str]) -> PackageSet:
|
||||
db.session.merge(package)
|
||||
|
||||
key = package.getId()
|
||||
print(f"Resolving for {key}", file=sys.stderr)
|
||||
|
||||
history = history.copy()
|
||||
history.append(key)
|
||||
|
||||
if package.type == PackageType.GAME:
|
||||
return PackageSet([package])
|
||||
|
||||
if key in self.resolved_packages:
|
||||
return self.resolved_packages.get(key)
|
||||
|
||||
if key in self.checked_packages:
|
||||
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
|
||||
return PackageSet()
|
||||
|
||||
self.checked_packages.add(key)
|
||||
|
||||
if package.type != PackageType.MOD:
|
||||
raise LogicError(500, "Got non-mod")
|
||||
|
||||
retval = PackageSet()
|
||||
|
||||
for dep in package.dependencies.filter_by(optional=False).all():
|
||||
ret = self.resolve_for_meta_package(dep.meta_package, history)
|
||||
if len(ret) == 0:
|
||||
continue
|
||||
elif len(retval) == 0:
|
||||
retval.update(ret)
|
||||
else:
|
||||
retval.intersection_update(ret)
|
||||
if len(retval) == 0:
|
||||
raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games")
|
||||
|
||||
self.resolved_packages[key] = retval
|
||||
return retval
|
||||
|
||||
def update_all(self) -> None:
|
||||
for package in Package.query.filter(Package.type == PackageType.MOD, Package.state != PackageState.DELETED).all():
|
||||
retval = self.resolve(package, [])
|
||||
for game in retval:
|
||||
support = PackageGameSupport(package, game)
|
||||
db.session.add(support)
|
||||
|
||||
def update(self, package: Package) -> None:
|
||||
previous_supported: Dict[str, PackageGameSupport] = {}
|
||||
for support in package.supported_games.all():
|
||||
previous_supported[support.game.getId()] = support
|
||||
|
||||
retval = self.resolve(package, [])
|
||||
for game in retval:
|
||||
assert game
|
||||
|
||||
lookup = previous_supported.pop(game.getId(), None)
|
||||
if lookup is None:
|
||||
support = PackageGameSupport(package, game)
|
||||
db.session.add(support)
|
||||
elif lookup.confidence == 0:
|
||||
lookup.supports = True
|
||||
db.session.merge(lookup)
|
||||
|
||||
for game, support in previous_supported.items():
|
||||
if support.confidence == 0:
|
||||
db.session.remove(support)
|
|
@ -159,7 +159,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
|||
raise LogicError(400, "Unknown tag: " + tag_id)
|
||||
|
||||
if not was_web and tag.is_protected:
|
||||
break
|
||||
continue
|
||||
|
||||
if tag.is_protected and tag not in old_tags and not user.rank.atLeast(UserRank.EDITOR):
|
||||
raise LogicError(400, lazy_gettext("Unable to add protected tag %(title)s to package", title=tag.title))
|
||||
|
|
|
@ -9,7 +9,7 @@ from app.utils import addNotification, addAuditLog
|
|||
from app.utils.image import get_image_size
|
||||
|
||||
|
||||
def do_create_screenshot(user: User, package: Package, title: str, file, reason: str = None):
|
||||
def do_create_screenshot(user: User, package: Package, title: str, file, is_cover_image: bool, reason: str = None):
|
||||
thirty_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=30)
|
||||
count = package.screenshots.filter(PackageScreenshot.created_at > thirty_minutes_ago).count()
|
||||
if count >= 20:
|
||||
|
@ -47,6 +47,10 @@ def do_create_screenshot(user: User, package: Package, title: str, file, reason:
|
|||
|
||||
db.session.commit()
|
||||
|
||||
if is_cover_image:
|
||||
package.cover_image = ss
|
||||
db.session.commit()
|
||||
|
||||
return ss
|
||||
|
||||
|
||||
|
@ -66,3 +70,18 @@ def do_order_screenshots(_user: User, package: Package, order: [any]):
|
|||
raise LogicError(400, "Invalid id, not a number: {}".format(json.dumps(ss_id)))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def do_set_cover_image(_user: User, package: Package, cover_image):
|
||||
try:
|
||||
cover_image = int(cover_image)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise LogicError(400, "Invalid id, not a number: {}".format(json.dumps(cover_image)))
|
||||
|
||||
for screenshot in package.screenshots.all():
|
||||
if screenshot.id == cover_image:
|
||||
package.cover_image = screenshot
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
raise LogicError(400, "Unable to find screenshot")
|
||||
|
|
|
@ -344,6 +344,25 @@ class Dependency(db.Model):
|
|||
return retval
|
||||
|
||||
|
||||
class PackageGameSupport(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False)
|
||||
package = db.relationship("Package", foreign_keys=[package_id])
|
||||
|
||||
game_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False)
|
||||
game = db.relationship("Package", foreign_keys=[game_id])
|
||||
|
||||
supports = db.Column(db.Boolean, nullable=False, default=True)
|
||||
confidence = db.Column(db.Integer, nullable=False, default=1)
|
||||
|
||||
__table_args__ = (db.UniqueConstraint("game_id", "package_id", name="_package_game_support_uc"),)
|
||||
|
||||
def __init__(self, package, game):
|
||||
self.package = package
|
||||
self.game = game
|
||||
|
||||
|
||||
class Package(db.Model):
|
||||
query_class = PackageQuery
|
||||
|
||||
|
@ -396,6 +415,12 @@ class Package(db.Model):
|
|||
|
||||
dependencies = db.relationship("Dependency", back_populates="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
|
||||
|
||||
supported_games = db.relationship("PackageGameSupport", back_populates="package", lazy="dynamic",
|
||||
foreign_keys=[PackageGameSupport.package_id])
|
||||
|
||||
game_supported_mods = db.relationship("PackageGameSupport", back_populates="game", lazy="dynamic",
|
||||
foreign_keys=[PackageGameSupport.game_id])
|
||||
|
||||
tags = db.relationship("Tag", secondary=Tags, back_populates="packages")
|
||||
|
||||
content_warnings = db.relationship("ContentWarning", secondary=ContentWarnings, back_populates="packages")
|
||||
|
@ -450,6 +475,14 @@ class Package(db.Model):
|
|||
for e in PackagePropertyKey:
|
||||
setattr(self, e.name, getattr(package, e.name))
|
||||
|
||||
@classmethod
|
||||
def get_by_key(cls, key):
|
||||
parts = key.split("/")
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
|
||||
return Package.query.filter(Package.name == parts[1], Package.author.has(username=parts[0])).first()
|
||||
|
||||
def getId(self):
|
||||
return "{}/{}".format(self.author.username, self.name)
|
||||
|
||||
|
@ -471,6 +504,11 @@ class Package(db.Model):
|
|||
def getSortedOptionalDependencies(self):
|
||||
return self.getSortedDependencies(False)
|
||||
|
||||
def getSortedSupportedGames(self):
|
||||
supported = self.supported_games.all()
|
||||
supported.sort(key=lambda x: -x.game.score)
|
||||
return supported
|
||||
|
||||
def getAsDictionaryKey(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
|
@ -542,7 +580,15 @@ class Package(db.Model):
|
|||
"release": release and release.id,
|
||||
|
||||
"score": round(self.score * 10) / 10,
|
||||
"downloads": self.downloads
|
||||
"downloads": self.downloads,
|
||||
|
||||
"game_support": [
|
||||
{
|
||||
"supports": support.supports,
|
||||
"confidence": support.confidence,
|
||||
"game": support.game.getAsDictionaryShort(base_url, version)
|
||||
} for support in self.supported_games.all()
|
||||
]
|
||||
}
|
||||
|
||||
def getThumbnailOrPlaceholder(self, level=2):
|
||||
|
@ -599,7 +645,7 @@ class Package(db.Model):
|
|||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return perm == Permission.SEE_PACKAGE and self.state == PackageState.APPROVED
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
|
@ -610,10 +656,7 @@ class Package(db.Model):
|
|||
isMaintainer = isOwner or user.rank.atLeast(UserRank.EDITOR) or user in self.maintainers
|
||||
isApprover = user.rank.atLeast(UserRank.APPROVER)
|
||||
|
||||
if perm == Permission.SEE_PACKAGE:
|
||||
return self.state == PackageState.APPROVED or isMaintainer or isApprover
|
||||
|
||||
elif perm == Permission.CREATE_THREAD:
|
||||
if perm == Permission.CREATE_THREAD:
|
||||
return user.rank.atLeast(UserRank.MEMBER)
|
||||
|
||||
# Members can edit their own packages, and editors can edit any packages
|
||||
|
@ -687,7 +730,8 @@ class Package(db.Model):
|
|||
needsScreenshot = \
|
||||
(self.type == self.type.GAME or self.type == self.type.TXP) and \
|
||||
self.screenshots.count() == 0
|
||||
return self.releases.count() > 0 and not needsScreenshot
|
||||
|
||||
return self.releases.filter(PackageRelease.task_id.is_(None)).count() > 0 and not needsScreenshot
|
||||
|
||||
elif state == PackageState.CHANGES_NEEDED:
|
||||
return self.checkPerm(user, Permission.APPROVE_NEW)
|
||||
|
@ -818,7 +862,13 @@ class Tag(db.Model):
|
|||
|
||||
def getAsDictionary(self):
|
||||
description = self.description if self.description != "" else None
|
||||
return { "name": self.name, "title": self.title, "description": description }
|
||||
return {
|
||||
"name": self.name,
|
||||
"title": self.title,
|
||||
"description": description,
|
||||
"is_protected": self.is_protected,
|
||||
"views": self.views,
|
||||
}
|
||||
|
||||
|
||||
class MinetestRelease(db.Model):
|
||||
|
@ -1042,8 +1092,11 @@ class PackageScreenshot(db.Model):
|
|||
"order": self.order,
|
||||
"title": self.title,
|
||||
"url": base_url + self.url,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"approved": self.approved,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"is_cover_image": self.package.cover_image == self,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -200,7 +200,8 @@ class PackageReview(db.Model):
|
|||
def getDeleteURL(self):
|
||||
return url_for("packages.delete_review",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name)
|
||||
name=self.package.name,
|
||||
reviewer=self.author.username)
|
||||
|
||||
def getVoteUrl(self, next_url=None):
|
||||
return url_for("packages.review_vote",
|
||||
|
@ -213,6 +214,20 @@ class PackageReview(db.Model):
|
|||
(pos, neg, _) = self.get_totals()
|
||||
self.score = 3 * (pos - neg) + 1
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to PackageReview.checkPerm()")
|
||||
|
||||
if perm == Permission.DELETE_REVIEW:
|
||||
return user == self.author or user.rank.atLeast(UserRank.MODERATOR)
|
||||
else:
|
||||
raise Exception("Permission {} is not related to reviews".format(perm.name))
|
||||
|
||||
|
||||
class PackageReviewVote(db.Model):
|
||||
review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), primary_key=True)
|
||||
|
|
|
@ -59,7 +59,6 @@ class UserRank(enum.Enum):
|
|||
|
||||
|
||||
class Permission(enum.Enum):
|
||||
SEE_PACKAGE = "SEE_PACKAGE"
|
||||
EDIT_PACKAGE = "EDIT_PACKAGE"
|
||||
DELETE_PACKAGE = "DELETE_PACKAGE"
|
||||
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
||||
|
@ -87,6 +86,7 @@ class Permission(enum.Enum):
|
|||
TOPIC_DISCARD = "TOPIC_DISCARD"
|
||||
CREATE_TOKEN = "CREATE_TOKEN"
|
||||
EDIT_MAINTAINERS = "EDIT_MAINTAINERS"
|
||||
DELETE_REVIEW = "DELETE_REVIEW"
|
||||
CHANGE_PROFILE_URLS = "CHANGE_PROFILE_URLS"
|
||||
CHANGE_DISPLAY_NAME = "CHANGE_DISPLAY_NAME"
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
$("textarea.markdown").each(function() {
|
||||
async function render(plainText, preview) {
|
||||
const response = await fetch(new Request("/api/markdown/", {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
const min = $("#min_rel");
|
||||
const max = $("#max_rel");
|
||||
const none = $("#min_rel option:first-child").attr("value");
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
$(".topic-discard").click(function() {
|
||||
const ele = $(this);
|
||||
const tid = ele.attr("data-tid");
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
document.querySelectorAll(".video-embed").forEach(ele => {
|
||||
try {
|
||||
const href = ele.getAttribute("href");
|
||||
|
|
|
@ -75,6 +75,10 @@ class QueryBuilder:
|
|||
if self.search is not None and self.search.strip() == "":
|
||||
self.search = None
|
||||
|
||||
self.game = args.get("game")
|
||||
if self.game:
|
||||
self.game = Package.get_by_key(self.game)
|
||||
|
||||
def setSortIfNone(self, name, dir="desc"):
|
||||
if self.order_by is None:
|
||||
self.order_by = name
|
||||
|
@ -132,6 +136,9 @@ class QueryBuilder:
|
|||
|
||||
query = query.filter_by(author=author)
|
||||
|
||||
if self.game:
|
||||
query = query.filter(Package.supported_games.any(game=self.game))
|
||||
|
||||
for tag in self.tags:
|
||||
query = query.filter(Package.tags.any(Tag.id == tag.id))
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import os, shutil, gitdb
|
||||
from zipfile import ZipFile
|
||||
|
@ -22,10 +23,11 @@ from kombu import uuid
|
|||
|
||||
from app.models import *
|
||||
from app.tasks import celery, TaskError
|
||||
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog, get_system_user
|
||||
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog
|
||||
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir
|
||||
from .minetestcheck import build_tree, MinetestCheckError, ContentType
|
||||
from ..logic.LogicError import LogicError
|
||||
from ..logic.game_support import GameSupportResolver
|
||||
from ..logic.packages import do_edit_package, ALIASES
|
||||
from ..utils.image import get_image_size
|
||||
|
||||
|
@ -113,6 +115,11 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
|
|||
for meta in getMetaPackages(optional_depends):
|
||||
db.session.add(Dependency(package, meta=meta, optional=True))
|
||||
|
||||
# Update game supports
|
||||
if package.type == PackageType.MOD:
|
||||
resolver = GameSupportResolver()
|
||||
resolver.update(package)
|
||||
|
||||
# Update min/max
|
||||
if tree.meta.get("min_minetest_version"):
|
||||
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
{% for title, group in notifications | select("package") | groupby("package.title") %}
|
||||
{% for title, group in notifications | selectattr("package") | groupby("package.title") %}
|
||||
<h2>
|
||||
{{ title }}
|
||||
</h2>
|
||||
|
@ -17,20 +17,23 @@
|
|||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
{% for group in notifications | reject("package") %}
|
||||
{% set other_notifications = notifications | selectattr("package", "none") %}
|
||||
|
||||
{% if other_notifications %}
|
||||
<h2>
|
||||
{{ _("Other Notifications") }}
|
||||
</h2>
|
||||
|
||||
<ul>
|
||||
{% for notification in group %}
|
||||
{% for notification in other_notifications %}
|
||||
<li>
|
||||
<a href="{{ notification.url | abs_url }}">{{ notification.title }}</a> -
|
||||
{{ _("from %(username)s.", username=notification.causer.username) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p style="margin-top: 3em;">
|
||||
<a class="btn" href="{{ abs_url_for('notifications.list_all') }}">
|
||||
{{ _("View Notifications") }}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% for entry in log %}
|
||||
<a class="list-group-item list-group-item-action"
|
||||
{% if entry.description and current_user.rank.atLeast(current_user.rank.MODERATOR) %}
|
||||
href="{{ url_for('admin.audit_view', id=entry.id) }}">
|
||||
href="{{ url_for('admin.audit_view', id_=entry.id) }}">
|
||||
{% else %}
|
||||
href="{{ entry.url }}">
|
||||
{% endif %}
|
||||
|
|
|
@ -14,19 +14,24 @@
|
|||
</div>
|
||||
|
||||
{% set level = "warning" %}
|
||||
{% if package.releases.count() == 0 %}
|
||||
{% if package.releases.filter_by(task_id=None).count() == 0 %}
|
||||
{% set message %}
|
||||
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
||||
{% if package.update_config %}
|
||||
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL("packages.create_release") }}">
|
||||
{{ _("Create first release") }}
|
||||
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL('packages.create_release') }}">
|
||||
{{ _("Create release") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL("packages.setup_releases") }}">
|
||||
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL('packages.setup_releases') }}">
|
||||
{{ _("Set up releases") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ _("You need to create a release before this package can be approved.") }}
|
||||
|
||||
{% if package.releases.count() == 0 %}
|
||||
{{ _("You need to create a release before this package can be approved.") }}
|
||||
{% else %}
|
||||
{{ _("Release is still importing, or has an error.") }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ _("A release is required before this package can be approved.") }}
|
||||
{% endif %}
|
||||
|
|
|
@ -4,18 +4,21 @@
|
|||
{{ mpackage.name }} - {{ _("Meta Packages") }}
|
||||
{% endblock %}
|
||||
|
||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ _("Meta Package \"%(name)s\"", name=mpackage.name) }}</h1>
|
||||
|
||||
<h2>{{ _("Provided By") }}</h2>
|
||||
|
||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||
{{ render_pkggrid(mpackage.packages.filter_by(state="APPROVED").all()) }}
|
||||
<h3>{{ _("Games") }}</h3>
|
||||
{{ render_pkggrid(mpackage.packages.filter_by(type="GAME", state="APPROVED").all()) }}
|
||||
|
||||
<h3>{{ _("Mods") }}</h3>
|
||||
{{ render_pkggrid(mpackage.packages.filter_by(type="MOD", state="APPROVED").all()) }}
|
||||
|
||||
{% if similar_topics %}
|
||||
<p>
|
||||
{{ _("Unfortunately, this isn't on ContentDB yet! Here's some forum topic(s):") }}
|
||||
</p>
|
||||
<h3>{{ _("Forum Topics") }}</h3>
|
||||
<ul>
|
||||
{% for t in similar_topics %}
|
||||
<li>
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ _("Community Hub") }} -
|
||||
{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block headextra %}
|
||||
<meta name="og:title" content="{{ self.title() }}"/>
|
||||
<meta name="og:description" content="{{ _('Mods for %(title)s', title=package.title) }}"/>
|
||||
<meta name="description" content="{{ _('Mods for %(title)s', title=package.title) }}"/>
|
||||
<meta name="og:url" content="{{ package.getURL('packages.game_hub', absolute=True) }}"/>
|
||||
{% if package.getMainScreenshotURL() %}
|
||||
<meta name="og:image" content="{{ package.getMainScreenshotURL(absolute=True) }}"/>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||
|
||||
<h1 class="mb-5">
|
||||
{{ _("Community Hub") }} -
|
||||
<a href="{{ package.getURL('packages.view') }}">
|
||||
{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<a href="{{ url_for('packages.list_all', sort='approved_at', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Recently Added") }}</h2>
|
||||
{{ render_pkggrid(new) }}
|
||||
|
||||
|
||||
<a href="{{ url_for('packages.list_all', sort='last_release', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Recently Updated") }}</h2>
|
||||
{{ render_pkggrid(updated) }}
|
||||
|
||||
|
||||
<a href="{{ url_for('packages.list_all', type='mod', sort='score', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Top Mods") }}</h2>
|
||||
{{ render_pkggrid(pop_mod) }}
|
||||
|
||||
|
||||
<a href="{{ url_for('packages.list_all', sort='reviews', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Highest Reviewed") }}</h2>
|
||||
{{ render_pkggrid(high_reviewed) }}
|
||||
|
||||
{% endblock %}
|
|
@ -32,8 +32,6 @@
|
|||
<p class="mt-3">
|
||||
{{ _("Note: Min and max versions will be used to hide the package on
|
||||
platforms not within the range.") }}
|
||||
{{ _("You cannot select the oldest version for min or the newest version
|
||||
for max as this does not make sense - you can't predict the future.") }}
|
||||
<br />
|
||||
{{ _("Leave both as None if in doubt.") }}
|
||||
</p>
|
||||
|
|
|
@ -62,11 +62,6 @@
|
|||
|
||||
{{ _("You can <a href='/help/package_config/'>set this automatically</a> in the .conf of your package.") }}
|
||||
</p>
|
||||
<p>
|
||||
{{ _("You cannot select the oldest version for min or the newest version
|
||||
for max as this does not make sense - you can't predict the future.") }}
|
||||
</p>
|
||||
|
||||
<p class="mt-5">
|
||||
{{ render_submit_field(form.submit) }}
|
||||
</p>
|
||||
|
|
|
@ -82,11 +82,6 @@
|
|||
<br />
|
||||
{{ _("Leave both as None if in doubt.") }}
|
||||
</p>
|
||||
<p>
|
||||
{{ _("You cannot select the oldest version for min or the newest version
|
||||
for max as this does not make sense - you can't predict the future.") }}
|
||||
</p>
|
||||
|
||||
<p class="mt-5">
|
||||
{{ render_submit_field(form.submit) }}
|
||||
</p>
|
||||
|
|
|
@ -322,6 +322,13 @@
|
|||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||
{{ render_pkggrid(packages_uses) }}
|
||||
{% endif %}
|
||||
|
||||
{% if package.type == package.type.GAME %}
|
||||
<h2>{{ _("Content") }}</h2>
|
||||
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
|
||||
{{ _("View content for game") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<aside class="col-md-3 info-sidebar">
|
||||
|
@ -371,6 +378,12 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if package.type == package.type.GAME %}
|
||||
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
|
||||
{{ _("View content for game") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if package.type != package.type.TXP %}
|
||||
<h3>{{ _("Dependencies") }}</h3>
|
||||
<dl>
|
||||
|
@ -419,6 +432,23 @@
|
|||
</dl>
|
||||
{% endif %}
|
||||
|
||||
{% if package.type == package.type.MOD %}
|
||||
<h3>{{ _("Compatible Games") }}</h3>
|
||||
{% for support in package.getSortedSupportedGames() %}
|
||||
<a class="badge badge-secondary"
|
||||
href="{{ support.game.getURL('packages.view') }}">
|
||||
{{ _("%(title)s by %(display_name)s",
|
||||
title=support.game.title, display_name=support.game.author.display_name) }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ _("No specific game is required") }}
|
||||
{% endfor %}
|
||||
<p class="text-muted small mt-2 mb-0">
|
||||
{{ _("This is an experimental feature.") }}
|
||||
{{ _("Supported games are determined by an algorithm, and may not be correct.") }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>
|
||||
{{ _("Information") }}
|
||||
</h3>
|
||||
|
|
|
@ -36,10 +36,16 @@
|
|||
<input type="submit" class="btn btn-primary" value="{{ _('Subscribe') }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if thread and thread.checkPerm(current_user, "DELETE_THREAD") %}
|
||||
{% if thread.checkPerm(current_user, "DELETE_THREAD") %}
|
||||
<a href="{{ url_for('threads.delete_thread', id=thread.id) }}" class="float-right mr-2 btn btn-danger">{{ _('Delete') }}</a>
|
||||
{% endif %}
|
||||
{% if thread and thread.checkPerm(current_user, "LOCK_THREAD") %}
|
||||
{% if thread.review and thread.review.checkPerm(current_user, "DELETE_REVIEW") and current_user.username != thread.review.author.username %}
|
||||
<form method="post" action="{{ thread.review.getDeleteURL() }}" class="float-right mr-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="submit" class="btn btn-danger" value="{{ _('Convert to Thread') }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if thread.checkPerm(current_user, "LOCK_THREAD") %}
|
||||
{% if thread.locked %}
|
||||
<form method="post" action="{{ url_for('threads.set_lock', id=thread.id, lock=0) }}" class="float-right mr-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
|
|
|
@ -18,8 +18,7 @@
|
|||
from functools import wraps
|
||||
from flask import abort, redirect, url_for, request
|
||||
from flask_login import current_user
|
||||
from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, \
|
||||
ThreadReply, Thread, PackageState, PackageType, PackageAlias
|
||||
from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageType, PackageAlias
|
||||
|
||||
|
||||
def getPackageByInfo(author, name):
|
||||
|
@ -40,15 +39,14 @@ def is_package_page(f):
|
|||
if not ("author" in kwargs and "name" in kwargs):
|
||||
abort(400)
|
||||
|
||||
author = kwargs.pop("author")
|
||||
name = kwargs.pop("name")
|
||||
author = kwargs["author"]
|
||||
name = kwargs["name"]
|
||||
|
||||
package = getPackageByInfo(author, name)
|
||||
if package is None:
|
||||
package = getPackageByInfo(author, name + "_game")
|
||||
if package and package.type == PackageType.GAME:
|
||||
args = dict(kwargs)
|
||||
args["author"] = author
|
||||
args["name"] = name + "_game"
|
||||
return redirect(url_for(request.endpoint, **args))
|
||||
|
||||
|
@ -61,6 +59,8 @@ def is_package_page(f):
|
|||
|
||||
abort(404)
|
||||
|
||||
del kwargs["author"]
|
||||
del kwargs["name"]
|
||||
return f(package=package, *args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
|
|
@ -30,7 +30,7 @@ services:
|
|||
|
||||
worker:
|
||||
build: .
|
||||
command: celery -A app.tasks.celery worker
|
||||
command: celery -A app.tasks.celery worker --concurrency 1
|
||||
env_file:
|
||||
- config.env
|
||||
environment:
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
# Developer Introduction
|
||||
|
||||
## Overview
|
||||
|
||||
ContentDB is a Python [Flask](https://flask.palletsprojects.com/en/2.0.x/) webservice.
|
||||
There's a PostgreSQL database, manipulated using the [SQLAlchemy ORM](https://docs.sqlalchemy.org/en/14/).
|
||||
|
||||
When a user makes a request, Python Flask will direct the request to a *route* in an *blueprint*.
|
||||
A [blueprint](https://flask.palletsprojects.com/en/2.0.x/blueprints/) is a Flask construct to hold a set of routes.
|
||||
Routes are implemented using Python, and likely to respond by using database *models* and rendering HTML *templates*.
|
||||
|
||||
Routes may also use functions in the `app/logic/` module, which is a directory containing reusable functions. This
|
||||
allows the API, background tasks, and the front-end to reuse code.
|
||||
|
||||
To avoid blocking web requests, background tasks run as
|
||||
[Celery](https://docs.celeryproject.org/en/stable/getting-started/introduction.html) tasks.
|
||||
|
||||
|
||||
## Locations
|
||||
|
||||
### The App
|
||||
|
||||
The `app` directory contains the Python Flask application.
|
||||
|
||||
* `blueprints` contains all the Python code behind each endpoint / route.
|
||||
* `templates` contains all the HTML templates used to generate responses. Each directory in here matches a directory in blueprints.
|
||||
* `models` contains all the database table classes. ContentDB uses [SQLAlchemy](https://docs.sqlalchemy.org/en/14/) to interact with PostgreSQL.
|
||||
* `flatpages` contains all the markdown user documentation, including `/help/`.
|
||||
* `public` contains files that should be added to the web server unedited. Examples include CSS libraries, images, and JS scripts.
|
||||
* `scss` contains the stylesheet files, that are compiled into CSS.
|
||||
* `tasks` contains the background tasks executed by [Celery](https://docs.celeryproject.org/en/stable/getting-started/introduction.html).
|
||||
* `logic` is a collection of reusable functions. For example, shared code to create a release or edit a package is here.
|
||||
* `tests` contains the Unit Tests and UI tests.
|
||||
* `utils` contain generic Python utilities, for example common code to manage Flask requests.
|
||||
|
||||
There are also a number of Python files in the `app` directory. The most important one is `querybuilder.py`,
|
||||
which is used to generate SQLAlachemy queries for packages and topics.
|
||||
|
||||
### Supporting directories
|
||||
|
||||
* `migrations` contains code to manage database updates.
|
||||
* `translations` contains user-maintained translations / locales.
|
||||
* `utils` contains bash scripts to aid development and deployment.
|
||||
|
||||
|
||||
## How to find stuff
|
||||
|
||||
Generally, you want to start by finding the endpoint and then seeing the code it calls.
|
||||
|
||||
Endpoints are sensibly organised in `app/blueprints`.
|
||||
|
||||
You can also use a file search. For example, to find the package edit endpoint, search for `"/packages/<author>/<name>/edit/"`.
|
||||
|
||||
|
||||
## Users and Permissions
|
||||
|
||||
Many routes need to check whether a user can do a particular thing. Rather than hard coding this,
|
||||
models tend to have a `checkPerm` function which takes a user and a `Permission`.
|
||||
|
||||
A permission may be something like `Permission.EDIT_PACKAGE` or `Permission.DELETE_THREAD`.
|
||||
|
||||
```bash
|
||||
if not package.checkPerm(current_user, Permission.EDIT_PACKAGE):
|
||||
abort(403)
|
||||
```
|
||||
|
||||
|
||||
## Translations
|
||||
|
||||
ContentDB uses [Flask-Babel](https://flask-babel.tkte.ch/) for translation. All strings need to be tagged using
|
||||
a gettext function.
|
||||
|
||||
### Translating templates (HTML)
|
||||
|
||||
```html
|
||||
<div class="something" title="{{ _('This is translatable now') }}">
|
||||
{{ _("Please remember to do something related to this page or something") }}
|
||||
</div>
|
||||
```
|
||||
|
||||
With parameters:
|
||||
|
||||
```html
|
||||
<p>
|
||||
{{ _("Hello %(username)s, you have %(count)d new messages", username=username, count=count) }}
|
||||
</p>
|
||||
```
|
||||
|
||||
See <https://pythonhosted.org/Flask-Babel/#flask.ext.babel.Babel.localeselector> and
|
||||
<https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xiv-i18n-and-l10n>.
|
||||
|
||||
### Translating Python
|
||||
|
||||
If the text is within a request, then you can use gettext like so:
|
||||
|
||||
```py
|
||||
flash(gettext("Some error message"), "danger")
|
||||
```
|
||||
|
||||
If the text is global, for example as part of a python class, then you need to use lazy_gettext:
|
||||
|
||||
```py
|
||||
class PackageForm(FlaskForm):
|
||||
title = StringField(lazy_gettext("Title (Human-readable)"), [InputRequired(), Length(1, 100)])
|
||||
```
|
|
@ -54,3 +54,5 @@ To hot/live update CDB whilst it is running, use:
|
|||
./utils/reload.sh
|
||||
|
||||
This will only work with python code and templates, it won't update tasks or config.
|
||||
|
||||
Now consider reading the [Developer Introduction](dev_intro.md).
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: e571b3498f9e
|
||||
Revises: 3710e5fbbe87
|
||||
Create Date: 2022-02-01 19:30:59.537512
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e571b3498f9e'
|
||||
down_revision = '3710e5fbbe87'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('package_game_support',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('package_id', sa.Integer(), nullable=False),
|
||||
sa.Column('game_id', sa.Integer(), nullable=False),
|
||||
sa.Column('supports', sa.Boolean(), nullable=False),
|
||||
sa.Column('confidence', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['game_id'], ['package.id'], ),
|
||||
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('game_id', 'package_id', name='_package_game_support_uc')
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('package_game_support')
|
|
@ -8,15 +8,16 @@ msgstr ""
|
|||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
|
||||
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
|
||||
"PO-Revision-Date: 2022-01-29 19:51+0000\n"
|
||||
"Last-Translator: debiankaios <info@debiankaios.de>\n"
|
||||
"Language-Team: German <https://hosted.weblate.org/projects/minetest/"
|
||||
"contentdb/de/>\n"
|
||||
"Language: de\n"
|
||||
"Language-Team: German "
|
||||
"<https://hosted.weblate.org/projects/minetest/contentdb/de/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.11-dev\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
|
||||
#: app/__init__.py:102
|
||||
|
@ -194,7 +195,7 @@ msgstr "Forumthema-ID"
|
|||
|
||||
#: app/blueprints/packages/packages.py:253
|
||||
msgid "Video URL"
|
||||
msgstr ""
|
||||
msgstr "Video-URL"
|
||||
|
||||
#: app/blueprints/packages/packages.py:271
|
||||
msgid "Unable to find that user"
|
||||
|
@ -930,6 +931,8 @@ msgid ""
|
|||
"Screenshot is too small, it should be at least %(width)s by %(height)s "
|
||||
"pixels"
|
||||
msgstr ""
|
||||
"Der Screenshot ist zu klein, er sollte mindestens %(width)s mal %(height)s "
|
||||
"Pixel groß sein"
|
||||
|
||||
#: app/logic/uploads.py:52
|
||||
#, python-format
|
||||
|
@ -1973,7 +1976,7 @@ msgstr "Tipp: Fügen Sie die URL eines Forumthemas ein"
|
|||
|
||||
#: app/templates/packages/create_edit.html:120
|
||||
msgid "YouTube videos will be shown in an embed."
|
||||
msgstr ""
|
||||
msgstr "YouTube-Videos werden in einer Einbettung angezeigt."
|
||||
|
||||
#: app/templates/packages/edit_maintainers.html:4
|
||||
msgid "Edit Maintainers"
|
||||
|
@ -2392,6 +2395,8 @@ msgid ""
|
|||
"The recommended resolution is 1920x1080, and screenshots must be at least"
|
||||
" %(width)dx%(height)d."
|
||||
msgstr ""
|
||||
"Die empfohlene Auflösung ist 1920x1080, und die Screenshots müssen "
|
||||
"mindestens %(width)dx%(height)d groß sein."
|
||||
|
||||
#: app/templates/packages/screenshots.html:11
|
||||
msgid "Add Image"
|
||||
|
@ -2403,15 +2408,15 @@ msgstr "Das oberste Bildschirmfoto wird als Vorschaubild für das Paket verwende
|
|||
|
||||
#: app/templates/packages/screenshots.html:39 app/templates/todo/user.html:77
|
||||
msgid "Way too small"
|
||||
msgstr ""
|
||||
msgstr "Viel zu klein"
|
||||
|
||||
#: app/templates/packages/screenshots.html:43 app/templates/todo/user.html:80
|
||||
msgid "Too small"
|
||||
msgstr ""
|
||||
msgstr "Zu klein"
|
||||
|
||||
#: app/templates/packages/screenshots.html:47 app/templates/todo/user.html:83
|
||||
msgid "Not HD"
|
||||
msgstr ""
|
||||
msgstr "Nicht HD"
|
||||
|
||||
#: app/templates/packages/screenshots.html:53
|
||||
msgid "Awaiting approval"
|
||||
|
@ -2431,11 +2436,11 @@ msgstr "Umsortieren benötigt JavaScript."
|
|||
|
||||
#: app/templates/packages/screenshots.html:100
|
||||
msgid "Videos"
|
||||
msgstr ""
|
||||
msgstr "Videos"
|
||||
|
||||
#: app/templates/packages/screenshots.html:102
|
||||
msgid "You can set a video on the Edit Details page"
|
||||
msgstr ""
|
||||
msgstr "Sie können ein Video auf der Seite \"Details bearbeiten\" einstellen"
|
||||
|
||||
#: app/templates/packages/share.html:10
|
||||
msgid "Links"
|
||||
|
@ -2518,21 +2523,21 @@ msgstr "Herunterladen"
|
|||
#: app/templates/packages/view.html:43
|
||||
#, python-format
|
||||
msgid "Minetest %(min)s - %(max)s"
|
||||
msgstr ""
|
||||
msgstr "Minetest %(min)s-%(max)s"
|
||||
|
||||
#: app/templates/packages/view.html:45
|
||||
#, python-format
|
||||
msgid "For Minetest %(min)s and above"
|
||||
msgstr ""
|
||||
msgstr "Für Minetest %(min)s und höher"
|
||||
|
||||
#: app/templates/packages/view.html:47
|
||||
#, python-format
|
||||
msgid "Minetest %(max)s and below"
|
||||
msgstr ""
|
||||
msgstr "Minetest %(max)s und niedriger"
|
||||
|
||||
#: app/templates/packages/view.html:67
|
||||
msgid "How do I install this?"
|
||||
msgstr ""
|
||||
msgstr "Wie tue ich das installieren?"
|
||||
|
||||
#: app/templates/packages/view.html:73
|
||||
msgid "No downloads available"
|
||||
|
@ -2946,19 +2951,22 @@ msgstr ""
|
|||
|
||||
#: app/templates/todo/user.html:68
|
||||
msgid "Small Screenshots"
|
||||
msgstr ""
|
||||
msgstr "Kleine Screenshots"
|
||||
|
||||
#: app/templates/todo/user.html:71
|
||||
msgid ""
|
||||
"These packages have screenshots that are too small, and should be "
|
||||
"replaced."
|
||||
msgstr ""
|
||||
"Diese Pakete haben Screenshots, die zu klein sind und ersetzt werden sollten."
|
||||
|
||||
#: app/templates/todo/user.html:72
|
||||
msgid ""
|
||||
"Red and orange are screenshots below the limit, and grey screenshots are "
|
||||
"below the recommended resolution."
|
||||
msgstr ""
|
||||
"Rot und orange sind Screenshots, die unter dem Grenzwert liegen, und graue "
|
||||
"Screenshots liegen unter der empfohlenen Auflösung."
|
||||
|
||||
#: app/templates/todo/user.html:127
|
||||
msgid "See All"
|
||||
|
@ -3494,18 +3502,23 @@ msgid ""
|
|||
"This will blacklist an email address, preventing ContentDB from ever "
|
||||
"sending emails to it - including password resets."
|
||||
msgstr ""
|
||||
"Damit wird eine E-Mail-Adresse auf eine schwarze Liste gesetzt, so dass "
|
||||
"ContentDB keine E-Mails mehr an diese Adresse senden kann - auch nicht zum "
|
||||
"Zurücksetzen von Passwörtern."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:20
|
||||
msgid "Please enter the email address you wish to blacklist."
|
||||
msgstr ""
|
||||
"Bitte geben Sie die E-Mail-Adresse ein, die Sie auf die schwarze Liste "
|
||||
"setzen möchten."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:21
|
||||
msgid "You will then need to confirm the email"
|
||||
msgstr ""
|
||||
msgstr "Sie müssen dann die E-Mail bestätigen"
|
||||
|
||||
#: app/templates/users/unsubscribe.html:33
|
||||
msgid "You may now unsubscribe."
|
||||
msgstr ""
|
||||
msgstr "Sie können sich jetzt abmelden."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:40
|
||||
#, python-format
|
||||
|
@ -3513,6 +3526,8 @@ msgid ""
|
|||
"Unsubscribing may prevent you from being able to sign into the account "
|
||||
"'%(display_name)s'"
|
||||
msgstr ""
|
||||
"Wenn Sie sich abmelden, können Sie sich möglicherweise nicht mehr bei dem "
|
||||
"Konto \"%(display_name)s\" anmelden"
|
||||
|
||||
#: app/templates/users/unsubscribe.html:44
|
||||
msgid ""
|
||||
|
@ -3520,14 +3535,18 @@ msgid ""
|
|||
"essential system emails.\n"
|
||||
"\t\t\t\t\tConsider editing your email notification preferences instead."
|
||||
msgstr ""
|
||||
"ContentDB wird nicht mehr in der Lage sein, \"Passwort vergessen\" und "
|
||||
"andere wichtige System-E-Mails zu versenden.\n"
|
||||
"\t\t\t\t\tSie sollten stattdessen Ihre E-Mail-Benachrichtigungseinstellungen "
|
||||
"bearbeiten."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:50
|
||||
msgid "You won't be able to use this email with ContentDB anymore."
|
||||
msgstr ""
|
||||
msgstr "Sie werden diese E-Mail nicht mehr mit ContentDB verwenden können."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:57
|
||||
msgid "Edit Notification Preferences"
|
||||
msgstr ""
|
||||
msgstr "Einstellungen für Benachrichtigungen bearbeiten"
|
||||
|
||||
#: app/utils/user.py:50
|
||||
msgid "You have a lot of notifications, you should either read or clear them"
|
||||
|
@ -3613,4 +3632,3 @@ msgstr ""
|
|||
#~ "Erwägen Sie, die Aktualisierungseinstellungen "
|
||||
#~ "so zu ändern, dass stattdessen "
|
||||
#~ "automatisch Releases erstellt werden."
|
||||
|
||||
|
|
|
@ -8,20 +8,21 @@ msgstr ""
|
|||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
|
||||
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
|
||||
"Last-Translator: pampogo kiraly <pampogo.kiraly@gmail.com>\n"
|
||||
"PO-Revision-Date: 2022-01-29 19:51+0000\n"
|
||||
"Last-Translator: Balázs Kovács <kovacs.balazs.ktk@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://hosted.weblate.org/projects/minetest/"
|
||||
"contentdb/hu/>\n"
|
||||
"Language: hu\n"
|
||||
"Language-Team: Hungarian "
|
||||
"<https://hosted.weblate.org/projects/minetest/contentdb/hu/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.11-dev\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
|
||||
#: app/__init__.py:102
|
||||
msgid "You have been banned."
|
||||
msgstr ""
|
||||
msgstr "Kitiltottak."
|
||||
|
||||
#: app/template_filters.py:52
|
||||
#, python-format
|
||||
|
@ -36,7 +37,7 @@ msgstr "Név"
|
|||
|
||||
#: app/blueprints/api/tokens.py:34
|
||||
msgid "Limit to package"
|
||||
msgstr ""
|
||||
msgstr "Csomagkorlát"
|
||||
|
||||
#: app/blueprints/api/tokens.py:36 app/blueprints/packages/packages.py:255
|
||||
#: app/blueprints/packages/packages.py:460
|
||||
|
@ -58,7 +59,7 @@ msgstr "Belépés sikertelen [err=gh-oauth-login-failed]"
|
|||
|
||||
#: app/blueprints/github/__init__.py:62
|
||||
msgid "Linked GitHub to account"
|
||||
msgstr ""
|
||||
msgstr "A GitHub-ot hozzákapcsoltuk a fiókhoz"
|
||||
|
||||
#: app/blueprints/github/__init__.py:65
|
||||
#, fuzzy
|
||||
|
@ -170,11 +171,11 @@ msgstr "Tartalomra vonatkozó figyelmeztetések"
|
|||
|
||||
#: app/blueprints/packages/packages.py:244 app/templates/packages/view.html:431
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
msgstr "Licenc"
|
||||
|
||||
#: app/blueprints/packages/packages.py:245
|
||||
msgid "Media License"
|
||||
msgstr ""
|
||||
msgstr "Média licenc"
|
||||
|
||||
#: app/blueprints/packages/packages.py:247
|
||||
msgid "Long Description (Markdown)"
|
||||
|
@ -182,7 +183,7 @@ msgstr "Hosszú Leírás (Markdown)"
|
|||
|
||||
#: app/blueprints/packages/packages.py:249
|
||||
msgid "VCS Repository URL"
|
||||
msgstr ""
|
||||
msgstr "VCS tároló URL"
|
||||
|
||||
#: app/blueprints/packages/packages.py:250 app/blueprints/users/settings.py:51
|
||||
msgid "Website URL"
|
||||
|
@ -199,7 +200,7 @@ msgstr "Fórum Téma ID"
|
|||
|
||||
#: app/blueprints/packages/packages.py:253
|
||||
msgid "Video URL"
|
||||
msgstr ""
|
||||
msgstr "Videó URL"
|
||||
|
||||
#: app/blueprints/packages/packages.py:271
|
||||
msgid "Unable to find that user"
|
||||
|
@ -222,6 +223,8 @@ msgstr "Nincs erre engedélye"
|
|||
#: app/blueprints/packages/packages.py:402
|
||||
msgid "Please comment what changes are needed in the approval thread"
|
||||
msgstr ""
|
||||
"Kérjük, hogy a jóváhagyási témában írja meg, milyen változtatásokra van "
|
||||
"szükség"
|
||||
|
||||
#: app/blueprints/packages/packages.py:423
|
||||
#: app/blueprints/packages/packages.py:439
|
||||
|
@ -278,7 +281,7 @@ msgstr "Fájl Feltöltés"
|
|||
|
||||
#: app/blueprints/packages/releases.py:57
|
||||
msgid "Git reference (ie: commit hash, branch, or tag)"
|
||||
msgstr ""
|
||||
msgstr "Git hivatkozás (azaz: commit hash, branch vagy tag)"
|
||||
|
||||
#: app/blueprints/packages/releases.py:59
|
||||
#: app/blueprints/packages/releases.py:70
|
||||
|
@ -299,7 +302,7 @@ msgstr "URL"
|
|||
|
||||
#: app/blueprints/packages/releases.py:68
|
||||
msgid "Task ID"
|
||||
msgstr ""
|
||||
msgstr "Feladat azonosítója"
|
||||
|
||||
#: app/blueprints/packages/releases.py:69
|
||||
#: app/blueprints/packages/screenshots.py:40
|
||||
|
@ -316,11 +319,11 @@ msgstr ".zip fájl feltöltése"
|
|||
|
||||
#: app/blueprints/packages/releases.py:188
|
||||
msgid "Set Min"
|
||||
msgstr ""
|
||||
msgstr "Minimum beállítása"
|
||||
|
||||
#: app/blueprints/packages/releases.py:191
|
||||
msgid "Set Max"
|
||||
msgstr ""
|
||||
msgstr "Maximum beállítása"
|
||||
|
||||
#: app/blueprints/packages/releases.py:194
|
||||
#, fuzzy
|
||||
|
@ -334,19 +337,19 @@ msgstr "Frissítés"
|
|||
#: app/blueprints/packages/releases.py:244
|
||||
#: app/templates/packages/update_config.html:25
|
||||
msgid "Trigger"
|
||||
msgstr ""
|
||||
msgstr "Trigger"
|
||||
|
||||
#: app/blueprints/packages/releases.py:245
|
||||
msgid "New Commit"
|
||||
msgstr ""
|
||||
msgstr "Új Commit"
|
||||
|
||||
#: app/blueprints/packages/releases.py:246 app/templates/admin/tags/list.html:8
|
||||
msgid "New Tag"
|
||||
msgstr ""
|
||||
msgstr "Új címke"
|
||||
|
||||
#: app/blueprints/packages/releases.py:248
|
||||
msgid "Branch name"
|
||||
msgstr ""
|
||||
msgstr "Branch neve"
|
||||
|
||||
#: app/blueprints/packages/releases.py:249
|
||||
#: app/templates/packages/update_config.html:38
|
||||
|
@ -374,6 +377,8 @@ msgstr "Automatizálás Kikapcsolása"
|
|||
#: app/blueprints/packages/releases.py:292
|
||||
msgid "Please add a Git repository URL in order to set up automatic releases"
|
||||
msgstr ""
|
||||
"Kérjük, adja meg egy Git tároló URL címét az automatikus kiadások "
|
||||
"beállításához"
|
||||
|
||||
#: app/blueprints/packages/releases.py:308
|
||||
#, fuzzy
|
||||
|
@ -430,14 +435,14 @@ msgstr "Borítókép"
|
|||
|
||||
#: app/blueprints/report/__init__.py:34
|
||||
msgid "Message"
|
||||
msgstr ""
|
||||
msgstr "Üzenet"
|
||||
|
||||
#: app/blueprints/report/__init__.py:35 app/templates/base.html:238
|
||||
#: app/templates/macros/threads.html:53 app/templates/packages/view.html:510
|
||||
#: app/templates/report/index.html:4 app/templates/report/index.html:10
|
||||
#: app/templates/users/profile.html:28
|
||||
msgid "Report"
|
||||
msgstr ""
|
||||
msgstr "Jelentés"
|
||||
|
||||
#: app/blueprints/threads/__init__.py:64
|
||||
msgid "Already subscribed!"
|
||||
|
@ -612,7 +617,7 @@ msgstr ""
|
|||
#: app/blueprints/users/account.py:150 app/blueprints/users/account.py:253
|
||||
#: app/blueprints/users/settings.py:142
|
||||
msgid "That email address has been unsubscribed/blacklisted, and cannot be used"
|
||||
msgstr ""
|
||||
msgstr "Ez az e-mail cím leiratkozott/fekete listára került és nem használható"
|
||||
|
||||
#: app/blueprints/users/account.py:190
|
||||
#, fuzzy
|
||||
|
@ -621,7 +626,7 @@ msgstr "Jelszó Visszaállítása"
|
|||
|
||||
#: app/blueprints/users/account.py:215
|
||||
msgid "Unable to find account"
|
||||
msgstr ""
|
||||
msgstr "Nem találja a fiókot"
|
||||
|
||||
#: app/blueprints/users/account.py:225 app/blueprints/users/account.py:232
|
||||
msgid "New password"
|
||||
|
@ -658,7 +663,7 @@ msgstr "Ismeretlen ellenőrző jelszó!"
|
|||
|
||||
#: app/blueprints/users/account.py:328
|
||||
msgid "Token has expired"
|
||||
msgstr ""
|
||||
msgstr "A token lejárt"
|
||||
|
||||
#: app/blueprints/users/account.py:342
|
||||
msgid "Another user is already using that email"
|
||||
|
@ -693,6 +698,8 @@ msgid ""
|
|||
"That email is now blacklisted. Please contact an admin if you wish to "
|
||||
"undo this."
|
||||
msgstr ""
|
||||
"Ez az e-mail mostantól feketelistán van. Kérjük, lépjen kapcsolatba egy "
|
||||
"adminisztrátorral, ha le szeretné venni onnan."
|
||||
|
||||
#: app/blueprints/users/claim.py:40 app/blueprints/users/claim.py:65
|
||||
#, fuzzy
|
||||
|
@ -705,11 +712,11 @@ msgstr ""
|
|||
|
||||
#: app/blueprints/users/claim.py:45
|
||||
msgid "User has already been claimed"
|
||||
msgstr ""
|
||||
msgstr "A felhasználónév már használatban van"
|
||||
|
||||
#: app/blueprints/users/claim.py:49
|
||||
msgid "Unable to get GitHub username for user"
|
||||
msgstr ""
|
||||
msgstr "Nem sikerült lekérni a felhasználó GitHub-felhasználónevét"
|
||||
|
||||
#: app/blueprints/users/claim.py:72
|
||||
msgid "That user has already been claimed!"
|
||||
|
@ -3447,4 +3454,3 @@ msgstr ""
|
|||
#~ "Consider changing the update settings to"
|
||||
#~ " create releases automatically instead."
|
||||
#~ msgstr ""
|
||||
|
||||
|
|
|
@ -8,16 +8,17 @@ msgstr ""
|
|||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
|
||||
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
|
||||
"PO-Revision-Date: 2022-01-31 14:54+0000\n"
|
||||
"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat "
|
||||
"Yasuyoshi <translation@mnh48.moe>\n"
|
||||
"Language-Team: Malay <https://hosted.weblate.org/projects/minetest/contentdb/"
|
||||
"ms/>\n"
|
||||
"Language: ms\n"
|
||||
"Language-Team: Malay "
|
||||
"<https://hosted.weblate.org/projects/minetest/contentdb/ms/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.11-dev\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
|
||||
#: app/__init__.py:102
|
||||
|
@ -195,7 +196,7 @@ msgstr "ID Topik Forum"
|
|||
|
||||
#: app/blueprints/packages/packages.py:253
|
||||
msgid "Video URL"
|
||||
msgstr ""
|
||||
msgstr "URL Video"
|
||||
|
||||
#: app/blueprints/packages/packages.py:271
|
||||
msgid "Unable to find that user"
|
||||
|
@ -926,6 +927,8 @@ msgid ""
|
|||
"Screenshot is too small, it should be at least %(width)s by %(height)s "
|
||||
"pixels"
|
||||
msgstr ""
|
||||
"Tangkap layar terlalu kecil, ia mestilah sekurang-kurangnya "
|
||||
"%(width)sx%(height)s piksel"
|
||||
|
||||
#: app/logic/uploads.py:52
|
||||
#, python-format
|
||||
|
@ -1424,8 +1427,8 @@ msgid ""
|
|||
"If you weren't expecting to receive this email, then you can safely "
|
||||
"ignore it."
|
||||
msgstr ""
|
||||
"Jika anda tidak menjangka untuk menerima e-mel ini, anda boleh abaikannya"
|
||||
" sahaja."
|
||||
"Jika anda tidak menyangka untuk menerima e-mel ini, anda boleh abaikannya "
|
||||
"sahaja."
|
||||
|
||||
#: app/templates/emails/verify.html:4
|
||||
#: app/templates/emails/verify_unsubscribe.html:5
|
||||
|
@ -1949,7 +1952,7 @@ msgstr "Petua: tampalkan URL topik forum"
|
|||
|
||||
#: app/templates/packages/create_edit.html:120
|
||||
msgid "YouTube videos will be shown in an embed."
|
||||
msgstr ""
|
||||
msgstr "Video YouTube akan dipaparkan dalam benaman."
|
||||
|
||||
#: app/templates/packages/edit_maintainers.html:4
|
||||
msgid "Edit Maintainers"
|
||||
|
@ -2361,6 +2364,8 @@ msgid ""
|
|||
"The recommended resolution is 1920x1080, and screenshots must be at least"
|
||||
" %(width)dx%(height)d."
|
||||
msgstr ""
|
||||
"Resolusi yang digalakkan ialah 1920x1080, dan tangkap layar mestilah "
|
||||
"sekurang-kurangnya %(width)dx%(height)d."
|
||||
|
||||
#: app/templates/packages/screenshots.html:11
|
||||
msgid "Add Image"
|
||||
|
@ -2372,15 +2377,15 @@ msgstr "Tangkap layar paling atas akan digunakan sebagai imej kenit pakej."
|
|||
|
||||
#: app/templates/packages/screenshots.html:39 app/templates/todo/user.html:77
|
||||
msgid "Way too small"
|
||||
msgstr ""
|
||||
msgstr "Terlalu kecil sangat"
|
||||
|
||||
#: app/templates/packages/screenshots.html:43 app/templates/todo/user.html:80
|
||||
msgid "Too small"
|
||||
msgstr ""
|
||||
msgstr "Kecil sangat"
|
||||
|
||||
#: app/templates/packages/screenshots.html:47 app/templates/todo/user.html:83
|
||||
msgid "Not HD"
|
||||
msgstr ""
|
||||
msgstr "Bukan HD"
|
||||
|
||||
#: app/templates/packages/screenshots.html:53
|
||||
msgid "Awaiting approval"
|
||||
|
@ -2400,11 +2405,11 @@ msgstr "Perubahan tertib memerlukan JavaScript."
|
|||
|
||||
#: app/templates/packages/screenshots.html:100
|
||||
msgid "Videos"
|
||||
msgstr ""
|
||||
msgstr "Video"
|
||||
|
||||
#: app/templates/packages/screenshots.html:102
|
||||
msgid "You can set a video on the Edit Details page"
|
||||
msgstr ""
|
||||
msgstr "Anda boleh tetapkan video di halaman Edit Maklumat"
|
||||
|
||||
#: app/templates/packages/share.html:10
|
||||
msgid "Links"
|
||||
|
@ -2485,21 +2490,21 @@ msgstr "Muat Turun"
|
|||
#: app/templates/packages/view.html:43
|
||||
#, python-format
|
||||
msgid "Minetest %(min)s - %(max)s"
|
||||
msgstr ""
|
||||
msgstr "Minetest %(min)s - %(max)s"
|
||||
|
||||
#: app/templates/packages/view.html:45
|
||||
#, python-format
|
||||
msgid "For Minetest %(min)s and above"
|
||||
msgstr ""
|
||||
msgstr "Untuk Minetest %(min)s dan ke atas"
|
||||
|
||||
#: app/templates/packages/view.html:47
|
||||
#, python-format
|
||||
msgid "Minetest %(max)s and below"
|
||||
msgstr ""
|
||||
msgstr "Untuk Minetest %(max)s dan ke bawah"
|
||||
|
||||
#: app/templates/packages/view.html:67
|
||||
msgid "How do I install this?"
|
||||
msgstr ""
|
||||
msgstr "Bagaimana untuk pasangkan ini?"
|
||||
|
||||
#: app/templates/packages/view.html:73
|
||||
msgid "No downloads available"
|
||||
|
@ -2911,19 +2916,23 @@ msgstr ""
|
|||
|
||||
#: app/templates/todo/user.html:68
|
||||
msgid "Small Screenshots"
|
||||
msgstr ""
|
||||
msgstr "Tangkap Layar Kecil"
|
||||
|
||||
#: app/templates/todo/user.html:71
|
||||
msgid ""
|
||||
"These packages have screenshots that are too small, and should be "
|
||||
"replaced."
|
||||
msgstr ""
|
||||
"Pakej-pakej ini mempunyai tangkap layar yang terlalu kecil, dan patut "
|
||||
"digantikan."
|
||||
|
||||
#: app/templates/todo/user.html:72
|
||||
msgid ""
|
||||
"Red and orange are screenshots below the limit, and grey screenshots are "
|
||||
"below the recommended resolution."
|
||||
msgstr ""
|
||||
"Yang warna merah dan jingga itu tangkap layar di bawah had minimum, manakala "
|
||||
"yang kelabu itu tangkap layar di bawah resolusi yang digalakkan."
|
||||
|
||||
#: app/templates/todo/user.html:127
|
||||
msgid "See All"
|
||||
|
@ -3451,18 +3460,20 @@ msgid ""
|
|||
"This will blacklist an email address, preventing ContentDB from ever "
|
||||
"sending emails to it - including password resets."
|
||||
msgstr ""
|
||||
"Ini akan menyenaraihitamkan alamat e-mel, mencegah ContentDB daripada "
|
||||
"menghantar sebarang e-mel kepadanya - termasuk penetapan semula kata laluan."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:20
|
||||
msgid "Please enter the email address you wish to blacklist."
|
||||
msgstr ""
|
||||
msgstr "Sila masukkan alamat e-mel yang anda ingin senaraihitamkan."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:21
|
||||
msgid "You will then need to confirm the email"
|
||||
msgstr ""
|
||||
msgstr "Kemudian anda perlu mengesahkan e-mel tersebut"
|
||||
|
||||
#: app/templates/users/unsubscribe.html:33
|
||||
msgid "You may now unsubscribe."
|
||||
msgstr ""
|
||||
msgstr "Kini anda boleh buang langganan."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:40
|
||||
#, python-format
|
||||
|
@ -3470,6 +3481,8 @@ msgid ""
|
|||
"Unsubscribing may prevent you from being able to sign into the account "
|
||||
"'%(display_name)s'"
|
||||
msgstr ""
|
||||
"Membuang langganan boleh menghalang anda daripada log masuk ke akaun "
|
||||
"'%(display_name)s'"
|
||||
|
||||
#: app/templates/users/unsubscribe.html:44
|
||||
msgid ""
|
||||
|
@ -3477,14 +3490,18 @@ msgid ""
|
|||
"essential system emails.\n"
|
||||
"\t\t\t\t\tConsider editing your email notification preferences instead."
|
||||
msgstr ""
|
||||
"ContentDB tidak akan mampu menghantar e-mel \"terlupa kata laluan\" dan e-"
|
||||
"mel sistem asas yang lain.\n"
|
||||
"\t\t\t\t\tSila pertimbangkan untuk edit keutamaan pemberitahuan e-mel "
|
||||
"menggantikan ini."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:50
|
||||
msgid "You won't be able to use this email with ContentDB anymore."
|
||||
msgstr ""
|
||||
msgstr "Anda tidak akan mampu menggunakan e-mel ini dengan ContentDB lagi."
|
||||
|
||||
#: app/templates/users/unsubscribe.html:57
|
||||
msgid "Edit Notification Preferences"
|
||||
msgstr ""
|
||||
msgstr "Edit Keutamaan Pemberitahuan"
|
||||
|
||||
#: app/utils/user.py:50
|
||||
msgid "You have a lot of notifications, you should either read or clear them"
|
||||
|
@ -3602,4 +3619,3 @@ msgstr ""
|
|||
#~ "Pertimbangkan untuk tukar tetapan kemas "
|
||||
#~ "kini untuk cipta terbitan secara "
|
||||
#~ "automatik sahaja."
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -8,20 +8,21 @@ msgstr ""
|
|||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
|
||||
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
|
||||
"PO-Revision-Date: 2022-02-01 14:55+0000\n"
|
||||
"Last-Translator: Y.W. <y5nw@outlook.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
|
||||
"minetest/contentdb/zh_Hans/>\n"
|
||||
"Language: zh_Hans\n"
|
||||
"Language-Team: Chinese (Simplified) "
|
||||
"<https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.11-dev\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
|
||||
#: app/__init__.py:102
|
||||
msgid "You have been banned."
|
||||
msgstr "你被封号了。"
|
||||
msgstr "您被封号了。"
|
||||
|
||||
#: app/template_filters.py:52
|
||||
#, python-format
|
||||
|
@ -58,15 +59,15 @@ msgstr "验证失败 [err=gh-oauth-login-failed]"
|
|||
|
||||
#: app/blueprints/github/__init__.py:62
|
||||
msgid "Linked GitHub to account"
|
||||
msgstr "绑定猫站账号"
|
||||
msgstr "已绑定GitHub账号"
|
||||
|
||||
#: app/blueprints/github/__init__.py:65
|
||||
msgid "GitHub account is already associated with another user"
|
||||
msgstr "猫站账号已与其他用户绑定"
|
||||
msgstr "GitHub账号已与其他用户绑定"
|
||||
|
||||
#: app/blueprints/github/__init__.py:71
|
||||
msgid "Unable to find an account for that GitHub user"
|
||||
msgstr "无法找到猫站用户的账号"
|
||||
msgstr "无法找到该GitHub用户的账号"
|
||||
|
||||
#: app/blueprints/github/__init__.py:76
|
||||
msgid "Authorization failed [err=gh-login-failed]"
|
||||
|
@ -192,7 +193,7 @@ msgstr "论坛贴子ID"
|
|||
|
||||
#: app/blueprints/packages/packages.py:253
|
||||
msgid "Video URL"
|
||||
msgstr ""
|
||||
msgstr "视频URL"
|
||||
|
||||
#: app/blueprints/packages/packages.py:271
|
||||
msgid "Unable to find that user"
|
||||
|
@ -415,7 +416,6 @@ msgid "Cover Image"
|
|||
msgstr "封面图像"
|
||||
|
||||
#: app/blueprints/report/__init__.py:34
|
||||
#, fuzzy
|
||||
msgid "Message"
|
||||
msgstr "消息"
|
||||
|
||||
|
@ -424,7 +424,7 @@ msgstr "消息"
|
|||
#: app/templates/report/index.html:4 app/templates/report/index.html:10
|
||||
#: app/templates/users/profile.html:28
|
||||
msgid "Report"
|
||||
msgstr ""
|
||||
msgstr "举报"
|
||||
|
||||
#: app/blueprints/threads/__init__.py:64
|
||||
msgid "Already subscribed!"
|
||||
|
@ -469,23 +469,23 @@ msgstr "评论需要在3到2000个字符之间。"
|
|||
#: app/blueprints/threads/__init__.py:275
|
||||
#: app/templates/macros/package_approval.html:107
|
||||
msgid "Open Thread"
|
||||
msgstr ""
|
||||
msgstr "打开贴子"
|
||||
|
||||
#: app/blueprints/threads/__init__.py:287
|
||||
msgid "Unable to find that package!"
|
||||
msgstr ""
|
||||
msgstr "找不到软件包!"
|
||||
|
||||
#: app/blueprints/threads/__init__.py:301
|
||||
msgid "Unable to create thread!"
|
||||
msgstr ""
|
||||
msgstr "无法创建贴子!"
|
||||
|
||||
#: app/blueprints/threads/__init__.py:306
|
||||
msgid "An approval thread already exists!"
|
||||
msgstr ""
|
||||
msgstr "审核贴已存在!"
|
||||
|
||||
#: app/blueprints/threads/__init__.py:310
|
||||
msgid "Please wait before opening another thread"
|
||||
msgstr ""
|
||||
msgstr "开启另外一个贴子前请等待"
|
||||
|
||||
#: app/blueprints/users/account.py:37 app/templates/users/login.html:15
|
||||
msgid "Username or email"
|
||||
|
@ -513,16 +513,15 @@ msgstr "电子邮件或密码不正确"
|
|||
#: app/blueprints/users/account.py:54
|
||||
#, python-format
|
||||
msgid "User %(username)s does not exist"
|
||||
msgstr ""
|
||||
msgstr "用户%(username)s不存在"
|
||||
|
||||
#: app/blueprints/users/account.py:57
|
||||
#, fuzzy
|
||||
msgid "Incorrect password. Did you set one?"
|
||||
msgstr "密码不正确。您设置了一个吗?"
|
||||
msgstr "密码不正确。您设置了吗?"
|
||||
|
||||
#: app/blueprints/users/account.py:60
|
||||
msgid "You need to confirm the registration email"
|
||||
msgstr "你需要确认注册的电子邮件"
|
||||
msgstr "你需要确认注册电子邮件"
|
||||
|
||||
#: app/blueprints/users/account.py:68
|
||||
msgid "Login failed"
|
||||
|
@ -530,7 +529,7 @@ msgstr "登录失败"
|
|||
|
||||
#: app/blueprints/users/account.py:103 app/blueprints/users/settings.py:50
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
msgstr "显示名称"
|
||||
|
||||
#: app/blueprints/users/account.py:104 app/blueprints/users/settings.py:263
|
||||
#: app/templates/users/list.html:18
|
||||
|
@ -541,7 +540,7 @@ msgstr "用户名"
|
|||
#: app/templates/users/claim_forums.html:68
|
||||
#: app/templates/users/register.html:16
|
||||
msgid "Only a-zA-Z0-9._ allowed"
|
||||
msgstr ""
|
||||
msgstr "只允许使用这些字符:a-zA-Z0-9._"
|
||||
|
||||
#: app/blueprints/users/account.py:106 app/blueprints/users/account.py:189
|
||||
#: app/blueprints/users/account.py:224 app/blueprints/users/account.py:376
|
||||
|
@ -551,7 +550,7 @@ msgstr "电子邮件"
|
|||
|
||||
#: app/blueprints/users/account.py:108
|
||||
msgid "What is the result of the above calculation?"
|
||||
msgstr ""
|
||||
msgstr "上述算式的计算结果是什么?"
|
||||
|
||||
#: app/blueprints/users/account.py:109
|
||||
msgid "I agree"
|
||||
|
@ -564,7 +563,7 @@ msgstr "注册"
|
|||
|
||||
#: app/blueprints/users/account.py:115
|
||||
msgid "Incorrect captcha answer"
|
||||
msgstr ""
|
||||
msgstr "验证码错误"
|
||||
|
||||
#: app/blueprints/users/account.py:119
|
||||
msgid "Username is invalid"
|
||||
|
@ -572,27 +571,27 @@ msgstr "用户名无效"
|
|||
|
||||
#: app/blueprints/users/account.py:130
|
||||
msgid "An account already exists for that username but hasn't been claimed yet."
|
||||
msgstr ""
|
||||
msgstr "该用户名已存在账户,但尚未被认领。"
|
||||
|
||||
#: app/blueprints/users/account.py:133 app/blueprints/users/account.py:140
|
||||
msgid "That username/display name is already in use, please choose another."
|
||||
msgstr ""
|
||||
msgstr "该用户名/显示名称已被使用,请选择另一个。"
|
||||
|
||||
#: app/blueprints/users/account.py:145 app/blueprints/users/account.py:258
|
||||
msgid "Email already in use"
|
||||
msgstr ""
|
||||
msgstr "电子邮件已使用"
|
||||
|
||||
#: app/blueprints/users/account.py:146 app/blueprints/users/account.py:259
|
||||
#, python-format
|
||||
msgid ""
|
||||
"We were unable to create the account as the email is already in use by "
|
||||
"%(display_name)s. Try a different email address."
|
||||
msgstr ""
|
||||
msgstr "我们无法创建该账户,因为该电子邮件已经被%(display_name)s使用。请尝试使用另一个电子邮件地址。"
|
||||
|
||||
#: app/blueprints/users/account.py:150 app/blueprints/users/account.py:253
|
||||
#: app/blueprints/users/settings.py:142
|
||||
msgid "That email address has been unsubscribed/blacklisted, and cannot be used"
|
||||
msgstr ""
|
||||
msgstr "该电子邮件地址已被取消关注/列入黑名单,不能再使用"
|
||||
|
||||
#: app/blueprints/users/account.py:190
|
||||
msgid "Reset Password"
|
||||
|
@ -607,7 +606,6 @@ msgid "New password"
|
|||
msgstr "新密码"
|
||||
|
||||
#: app/blueprints/users/account.py:226 app/blueprints/users/account.py:233
|
||||
#, fuzzy
|
||||
msgid "Verify password"
|
||||
msgstr "确认密码"
|
||||
|
||||
|
@ -633,11 +631,11 @@ msgstr "旧密码不正确"
|
|||
|
||||
#: app/blueprints/users/account.py:322
|
||||
msgid "Unknown verification token!"
|
||||
msgstr ""
|
||||
msgstr "未知验证口令!"
|
||||
|
||||
#: app/blueprints/users/account.py:328
|
||||
msgid "Token has expired"
|
||||
msgstr ""
|
||||
msgstr "口令已过期"
|
||||
|
||||
#: app/blueprints/users/account.py:342
|
||||
msgid "Another user is already using that email"
|
||||
|
@ -645,21 +643,19 @@ msgstr "另一个用户已经在使用该电子邮件"
|
|||
|
||||
#: app/blueprints/users/account.py:345
|
||||
msgid "Confirmed email change"
|
||||
msgstr ""
|
||||
msgstr "已确认的电子邮件变更"
|
||||
|
||||
#: app/blueprints/users/account.py:350
|
||||
msgid "Email address changed"
|
||||
msgstr ""
|
||||
msgstr "电子邮箱已变更"
|
||||
|
||||
#: app/blueprints/users/account.py:351
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Your email address has changed. If you didn't request this, please "
|
||||
"contact an administrator."
|
||||
msgstr "您的电子邮件地址已被更改。如果您没有请求更改电子邮件地址,请与管理员联系。"
|
||||
|
||||
#: app/blueprints/users/account.py:369
|
||||
#, fuzzy
|
||||
msgid "You may now log in"
|
||||
msgstr "您现在可以登录了"
|
||||
|
||||
|
@ -668,7 +664,6 @@ msgid "Send"
|
|||
msgstr "发送"
|
||||
|
||||
#: app/blueprints/users/account.py:408
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"That email is now blacklisted. Please contact an admin if you wish to "
|
||||
"undo this."
|
||||
|
@ -678,11 +673,12 @@ msgstr "该电子邮件现在已被列入黑名单。如果您希望撤消此操
|
|||
msgid ""
|
||||
"Invalid username - must only contain A-Za-z0-9._. Consider contacting an "
|
||||
"admin"
|
||||
msgstr ""
|
||||
msgstr "用户名只能包含 A-Za-z0-9._。请考虑联系管理员"
|
||||
|
||||
#: app/blueprints/users/claim.py:45
|
||||
#, fuzzy
|
||||
msgid "User has already been claimed"
|
||||
msgstr ""
|
||||
msgstr "用户已被认领"
|
||||
|
||||
#: app/blueprints/users/claim.py:49
|
||||
msgid "Unable to get GitHub username for user"
|
||||
|
@ -690,16 +686,16 @@ msgstr "无法获取用户的 GitHub 用户名"
|
|||
|
||||
#: app/blueprints/users/claim.py:72
|
||||
msgid "That user has already been claimed!"
|
||||
msgstr ""
|
||||
msgstr "该用户已被认领!"
|
||||
|
||||
#: app/blueprints/users/claim.py:86
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Error whilst attempting to access forums: %(message)s"
|
||||
msgstr "尝试访问论坛时出错:%(message)s"
|
||||
|
||||
#: app/blueprints/users/claim.py:90
|
||||
msgid "Unable to get forum signature - does the user exist?"
|
||||
msgstr ""
|
||||
msgstr "无法获得论坛签名 - 用户是否存在?"
|
||||
|
||||
#: app/blueprints/users/claim.py:105
|
||||
msgid "Unable to login as user"
|
||||
|
@ -707,15 +703,15 @@ msgstr "无法以用户身份登录"
|
|||
|
||||
#: app/blueprints/users/claim.py:111
|
||||
msgid "Could not find the key in your signature!"
|
||||
msgstr ""
|
||||
msgstr "在您的签名中找不到密钥!"
|
||||
|
||||
#: app/blueprints/users/claim.py:114
|
||||
msgid "Unknown claim type"
|
||||
msgstr ""
|
||||
msgstr "未知的认领类型"
|
||||
|
||||
#: app/blueprints/users/profile.py:112
|
||||
msgid "Top reviewer"
|
||||
msgstr ""
|
||||
msgstr "顶级评论者"
|
||||
|
||||
#: app/blueprints/users/profile.py:113
|
||||
#, python-format
|
||||
|
@ -724,60 +720,59 @@ msgstr "%(display_name)s在ContentDB上写了最有帮助的评论。"
|
|||
|
||||
#: app/blueprints/users/profile.py:118
|
||||
msgid "2nd most helpful reviewer"
|
||||
msgstr ""
|
||||
msgstr "第二位最有帮助的评论者"
|
||||
|
||||
#: app/blueprints/users/profile.py:120
|
||||
msgid "3rd most helpful reviewer"
|
||||
msgstr ""
|
||||
msgstr "第三位最有帮助的评论者"
|
||||
|
||||
#: app/blueprints/users/profile.py:121
|
||||
#, python-format
|
||||
msgid "This puts %(display_name)s in the top %(perc)s%%"
|
||||
msgstr ""
|
||||
msgstr "这使%(display_name)s位于顶部%(perc)s%%"
|
||||
|
||||
#: app/blueprints/users/profile.py:125
|
||||
#, python-format
|
||||
msgid "Top %(perc)s%% reviewer"
|
||||
msgstr ""
|
||||
msgstr "最高 %(perc)s%% 评论者"
|
||||
|
||||
#: app/blueprints/users/profile.py:126
|
||||
#, python-format
|
||||
msgid "Only %(place)d users have written more helpful reviews."
|
||||
msgstr ""
|
||||
msgstr "只有%(place)d个用户写了更多有帮助的评论。"
|
||||
|
||||
#: app/blueprints/users/profile.py:131
|
||||
#, fuzzy
|
||||
msgid "Consider writing more helpful reviews to get a medal."
|
||||
msgstr "请考虑写更多有帮助的评论以获得奖章。"
|
||||
|
||||
#: app/blueprints/users/profile.py:133
|
||||
#, python-format
|
||||
msgid "You are in place %(place)s."
|
||||
msgstr ""
|
||||
msgstr "你在第%(place)s位。"
|
||||
|
||||
#: app/blueprints/users/profile.py:161
|
||||
#, python-format
|
||||
msgid "Top %(type)s"
|
||||
msgstr ""
|
||||
msgstr "最高%(type)s"
|
||||
|
||||
#: app/blueprints/users/profile.py:163
|
||||
#, python-format
|
||||
msgid "Top %(group)d %(type)s"
|
||||
msgstr ""
|
||||
msgstr "最高 %(group)d %(type)s"
|
||||
|
||||
#: app/blueprints/users/profile.py:172
|
||||
#, python-format
|
||||
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
|
||||
msgstr ""
|
||||
msgstr "%(display_name)s 有一个 %(type)s 放置在 #%(place)d 处。"
|
||||
|
||||
#: app/blueprints/users/profile.py:187
|
||||
#, python-format
|
||||
msgid "Your packages have %(downloads)d downloads in total."
|
||||
msgstr ""
|
||||
msgstr "您的软件包总共有 %(downloads)d 次下载。"
|
||||
|
||||
#: app/blueprints/users/profile.py:188
|
||||
msgid "First medal is at 50k."
|
||||
msgstr ""
|
||||
msgstr "第一张奖章为50k。"
|
||||
|
||||
#: app/blueprints/users/profile.py:193
|
||||
msgid ">300k downloads"
|
||||
|
@ -856,19 +851,20 @@ msgid "Can't promote a user to a rank higher than yourself!"
|
|||
msgstr ""
|
||||
|
||||
#: app/logic/packages.py:95
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Name can only contain lower case letters (a-z), digits (0-9), and "
|
||||
"underscores (_)"
|
||||
msgstr "名称只能包含小写字母(a-z)、数字(0-9)和下划线(_)。"
|
||||
msgstr "名称只能包含小写字母(a-z)、数字(0-9)和下划线(_)"
|
||||
|
||||
#: app/logic/packages.py:109
|
||||
#, fuzzy
|
||||
msgid "You do not have permission to edit this package"
|
||||
msgstr ""
|
||||
msgstr "您没有权限编辑这个软件包"
|
||||
|
||||
#: app/logic/packages.py:113
|
||||
#, fuzzy
|
||||
msgid "You do not have permission to change the package name"
|
||||
msgstr ""
|
||||
msgstr "您没有权限修改这个软件包的名字"
|
||||
|
||||
#: app/logic/packages.py:165
|
||||
#, python-format
|
||||
|
@ -876,8 +872,9 @@ msgid "Unable to add protected tag %(title)s to package"
|
|||
msgstr ""
|
||||
|
||||
#: app/logic/releases.py:32
|
||||
#, fuzzy
|
||||
msgid "You do not have permission to make releases"
|
||||
msgstr ""
|
||||
msgstr "您没有权限发布新版本"
|
||||
|
||||
#: app/logic/releases.py:37
|
||||
msgid ""
|
||||
|
@ -905,7 +902,7 @@ msgid ""
|
|||
msgstr ""
|
||||
|
||||
#: app/logic/uploads.py:52
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Please upload %(file_desc)s"
|
||||
msgstr "请上传%(file_desc)s"
|
||||
|
||||
|
@ -914,33 +911,36 @@ msgid "Uploaded image isn't actually an image"
|
|||
msgstr ""
|
||||
|
||||
#: app/models/packages.py:65
|
||||
#, fuzzy
|
||||
msgid "Mod"
|
||||
msgstr "Mod"
|
||||
|
||||
#: app/models/packages.py:67
|
||||
#, fuzzy
|
||||
msgid "Game"
|
||||
msgstr ""
|
||||
msgstr "子游戏"
|
||||
|
||||
#: app/models/packages.py:69
|
||||
#, fuzzy
|
||||
msgid "Texture Pack"
|
||||
msgstr ""
|
||||
msgstr "材质包"
|
||||
|
||||
#: app/models/packages.py:74 app/templates/base.html:27
|
||||
msgid "Mods"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/packages.py:76 app/templates/base.html:30
|
||||
#, fuzzy
|
||||
msgid "Games"
|
||||
msgstr ""
|
||||
msgstr "子游戏"
|
||||
|
||||
#: app/models/packages.py:78 app/templates/base.html:33
|
||||
#, fuzzy
|
||||
msgid "Texture Packs"
|
||||
msgstr ""
|
||||
msgstr "材质包"
|
||||
|
||||
#: app/models/packages.py:167
|
||||
msgid "Submit for Approval"
|
||||
msgstr ""
|
||||
msgstr "提交并申请批准"
|
||||
|
||||
#: app/models/packages.py:169
|
||||
msgid "Approve"
|
||||
|
@ -956,10 +956,11 @@ msgid "Delete"
|
|||
msgstr "删除"
|
||||
|
||||
#: app/tasks/emails.py:102
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"You are receiving this email because you are a registered user of "
|
||||
"ContentDB."
|
||||
msgstr ""
|
||||
msgstr "您是ContentDB的一名用户,所以收到了这封邮件。"
|
||||
|
||||
#: app/tasks/emails.py:109 app/templates/emails/verify.html:30
|
||||
msgid ""
|
||||
|
@ -968,7 +969,7 @@ msgid ""
|
|||
msgstr ""
|
||||
|
||||
#: app/tasks/emails.py:143
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%(num)d new notifications"
|
||||
msgstr "%(num)d个新通知"
|
||||
|
||||
|
@ -990,7 +991,7 @@ msgstr "管理电子邮件设置"
|
|||
#: app/templates/threads/view.html:31 app/templates/users/unsubscribe.html:4
|
||||
#: app/templates/users/unsubscribe.html:61
|
||||
msgid "Unsubscribe"
|
||||
msgstr "取消订阅"
|
||||
msgstr "取消关注"
|
||||
|
||||
#: app/templates/404.html:4
|
||||
msgid "Page not found"
|
||||
|
@ -1044,12 +1045,14 @@ msgid "Notifications"
|
|||
msgstr "通知"
|
||||
|
||||
#: app/templates/base.html:105
|
||||
#, fuzzy
|
||||
msgid "Add Package"
|
||||
msgstr ""
|
||||
msgstr "添加软件包"
|
||||
|
||||
#: app/templates/base.html:122
|
||||
#, fuzzy
|
||||
msgid "Profile"
|
||||
msgstr ""
|
||||
msgstr "个人资料"
|
||||
|
||||
#: app/templates/base.html:140
|
||||
msgid "Admin"
|
||||
|
@ -1076,8 +1079,9 @@ msgid "Settings"
|
|||
msgstr "设置"
|
||||
|
||||
#: app/templates/base.html:161
|
||||
#, fuzzy
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
msgstr "退出"
|
||||
|
||||
#: app/templates/base.html:190
|
||||
msgid "Help translate ContentDB"
|
||||
|
@ -1125,7 +1129,6 @@ msgstr ""
|
|||
#: app/templates/index.html:69 app/templates/packages/reviews_list.html:4
|
||||
#: app/templates/packages/view.html:177 app/templates/packages/view.html:293
|
||||
#: app/templates/users/profile.html:206
|
||||
#, fuzzy
|
||||
msgid "Reviews"
|
||||
msgstr "评价"
|
||||
|
||||
|
@ -1145,16 +1148,18 @@ msgstr ""
|
|||
#: app/templates/index.html:115 app/templates/index.html:122
|
||||
#: app/templates/index.html:129 app/templates/index.html:151
|
||||
#: app/templates/index.html:158
|
||||
#, fuzzy
|
||||
msgid "See more"
|
||||
msgstr ""
|
||||
msgstr "查看更多"
|
||||
|
||||
#: app/templates/index.html:103
|
||||
msgid "Recently Added"
|
||||
msgstr "最近添加的"
|
||||
|
||||
#: app/templates/index.html:110
|
||||
#, fuzzy
|
||||
msgid "Recently Updated"
|
||||
msgstr ""
|
||||
msgstr "最近更新的"
|
||||
|
||||
#: app/templates/index.html:117
|
||||
msgid "Top Games"
|
||||
|
@ -1173,8 +1178,9 @@ msgid "Search by Tags"
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/index.html:153
|
||||
#, fuzzy
|
||||
msgid "Highest Reviewed"
|
||||
msgstr ""
|
||||
msgstr "评价最高的"
|
||||
|
||||
#: app/templates/index.html:160
|
||||
msgid "Recent Positive Reviews"
|
||||
|
@ -1203,13 +1209,14 @@ msgid "Send bulk email"
|
|||
msgstr "批量发送电子邮件"
|
||||
|
||||
#: app/templates/admin/send_bulk_notification.html:4
|
||||
#, fuzzy
|
||||
msgid "Send bulk notification"
|
||||
msgstr ""
|
||||
msgstr "批量发送消息"
|
||||
|
||||
#: app/templates/admin/send_email.html:4
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
msgid "Send email to %(username)s"
|
||||
msgstr ""
|
||||
msgstr "给%(username)s发送电子邮件"
|
||||
|
||||
#: app/templates/admin/licenses/list.html:8
|
||||
msgid "New License"
|
||||
|
@ -1239,17 +1246,20 @@ msgstr ""
|
|||
|
||||
#: app/templates/admin/versions/list.html:4
|
||||
#: app/templates/admin/versions/list.html:10
|
||||
#, fuzzy
|
||||
msgid "Minetest Versions"
|
||||
msgstr ""
|
||||
msgstr "Minetest版本"
|
||||
|
||||
#: app/templates/admin/versions/list.html:8
|
||||
#, fuzzy
|
||||
msgid "New Version"
|
||||
msgstr ""
|
||||
msgstr "新版本"
|
||||
|
||||
#: app/templates/admin/warnings/list.html:4
|
||||
#: app/templates/admin/warnings/list.html:10
|
||||
#, fuzzy
|
||||
msgid "Warnings"
|
||||
msgstr ""
|
||||
msgstr "警告"
|
||||
|
||||
#: app/templates/admin/warnings/list.html:8
|
||||
msgid "New Warning"
|
||||
|
@ -1285,8 +1295,9 @@ msgid ""
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/api/create_edit_token.html:40
|
||||
#, fuzzy
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
msgstr "重置"
|
||||
|
||||
#: app/templates/api/create_edit_token.html:49
|
||||
msgid "Human-readable name to tell tokens apart."
|
||||
|
@ -1305,12 +1316,14 @@ msgstr ""
|
|||
#: app/templates/macros/topics.html:65
|
||||
#: app/templates/packages/alias_list.html:13
|
||||
#: app/templates/packages/releases_list.html:29
|
||||
#, fuzzy
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
msgstr "创建"
|
||||
|
||||
#: app/templates/api/list_tokens.html:9
|
||||
#, fuzzy
|
||||
msgid "API Documentation"
|
||||
msgstr ""
|
||||
msgstr "API文档"
|
||||
|
||||
#: app/templates/api/list_tokens.html:19
|
||||
msgid "No tokens created"
|
||||
|
@ -1327,8 +1340,9 @@ msgid "From %(username)s."
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/emails/notification.html:19
|
||||
#, fuzzy
|
||||
msgid "View Notification"
|
||||
msgstr ""
|
||||
msgstr "查看消息"
|
||||
|
||||
#: app/templates/emails/notification.html:26
|
||||
#: app/templates/emails/notification_digest.html:43
|
||||
|
@ -1354,12 +1368,14 @@ msgid "from %(username)s."
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/emails/notification_digest.html:22
|
||||
#, fuzzy
|
||||
msgid "Other Notifications"
|
||||
msgstr ""
|
||||
msgstr "其它消息"
|
||||
|
||||
#: app/templates/emails/notification_digest.html:36
|
||||
#, fuzzy
|
||||
msgid "View Notifications"
|
||||
msgstr ""
|
||||
msgstr "查看消息"
|
||||
|
||||
#: app/templates/emails/unable_to_find_account.html:2
|
||||
msgid ""
|
||||
|
@ -1407,8 +1423,9 @@ msgid "If this was you, then please click this link to confirm the address:"
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/emails/verify.html:19
|
||||
#, fuzzy
|
||||
msgid "Confirm Email Address"
|
||||
msgstr ""
|
||||
msgstr "确认电子邮箱地址"
|
||||
|
||||
#: app/templates/emails/verify.html:23
|
||||
#: app/templates/emails/verify_unsubscribe.html:17
|
||||
|
@ -1445,12 +1462,14 @@ msgid "Start typing to see suggestions"
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/macros/package_approval.html:5 app/templates/todo/user.html:35
|
||||
#, fuzzy
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
msgstr "状态"
|
||||
|
||||
#: app/templates/macros/package_approval.html:22
|
||||
#, fuzzy
|
||||
msgid "Create first release"
|
||||
msgstr ""
|
||||
msgstr "发布第一个版本"
|
||||
|
||||
#: app/templates/macros/package_approval.html:26
|
||||
msgid "Set up releases"
|
||||
|
@ -1660,12 +1679,14 @@ msgid "No outdated packages."
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/macros/topics.html:6 app/templates/packages/view.html:160
|
||||
#, fuzzy
|
||||
msgid "Author"
|
||||
msgstr ""
|
||||
msgstr "作者"
|
||||
|
||||
#: app/templates/macros/topics.html:8
|
||||
#, fuzzy
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
msgstr "日期"
|
||||
|
||||
#: app/templates/macros/topics.html:9
|
||||
msgid "Actions"
|
||||
|
@ -1679,8 +1700,9 @@ msgid "WIP"
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/macros/topics.html:35
|
||||
#, fuzzy
|
||||
msgid "Show"
|
||||
msgstr ""
|
||||
msgstr "显示"
|
||||
|
||||
#: app/templates/macros/topics.html:37
|
||||
msgid "Discard"
|
||||
|
@ -2680,9 +2702,8 @@ msgid "Missing tags only"
|
|||
msgstr ""
|
||||
|
||||
#: app/templates/todo/tags.html:31
|
||||
#, fuzzy
|
||||
msgid "Edit Tags"
|
||||
msgstr "编辑详细信息"
|
||||
msgstr "编辑标签"
|
||||
|
||||
#: app/templates/todo/todo_base.html:11 app/templates/todo/user.html:4
|
||||
#, python-format
|
||||
|
@ -3370,4 +3391,3 @@ msgstr ""
|
|||
#~ "Consider changing the update settings to"
|
||||
#~ " create releases automatically instead."
|
||||
#~ msgstr ""
|
||||
|
||||
|
|
Loading…
Reference in New Issue