mirror of
https://github.com/minetest/contentdb.git
synced 2024-11-08 08:33:45 +01:00
parent
e5a4161e76
commit
1018e1c29c
@ -250,6 +250,7 @@ class PackageForm(FlaskForm):
|
||||
website = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None])
|
||||
issueTracker = StringField(lazy_gettext("Issue Tracker URL"), [Optional(), URL()], filters = [lambda x: x or None])
|
||||
forums = IntegerField(lazy_gettext("Forum Topic ID"), [Optional(), NumberRange(0,999999)])
|
||||
video_url = StringField(lazy_gettext("Video URL"), [Optional(), URL()], filters = [lambda x: x or None])
|
||||
|
||||
submit = SubmitField(lazy_gettext("Save"))
|
||||
|
||||
@ -333,6 +334,7 @@ def create_edit(author=None, name=None):
|
||||
"website": form.website.data,
|
||||
"issueTracker": form.issueTracker.data,
|
||||
"forums": form.forums.data,
|
||||
"video_url": form.video_url.data,
|
||||
})
|
||||
|
||||
if wasNew and package.repo is not None:
|
||||
|
@ -89,6 +89,7 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
|
||||
* `website`: Website URL.
|
||||
* `issue_tracker`: Issue tracker URL.
|
||||
* `forums`: forum topic ID.
|
||||
* `video_url`: URL to a video, YouTube only for now.
|
||||
* GET `/api/packages/<username>/<name>/dependencies/`
|
||||
* Returns dependencies, with suggested candidates
|
||||
* If query argument `only_hard` is present, only hard deps will be returned.
|
||||
|
@ -61,6 +61,7 @@ It should be a JSON dictionary with one or more of the following optional keys:
|
||||
* `website`: Website URL.
|
||||
* `issue_tracker`: Issue tracker URL.
|
||||
* `forums`: forum topic ID.
|
||||
* `video_url`: URL to a video, YouTube only for now.
|
||||
|
||||
Use `null` to unset fields where relevant.
|
||||
|
||||
|
@ -23,6 +23,7 @@ from app.logic.LogicError import LogicError
|
||||
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \
|
||||
License, UserRank, PackageDevState
|
||||
from app.utils import addAuditLog
|
||||
from app.utils.url import clean_youtube_url
|
||||
|
||||
|
||||
def check(cond: bool, msg: str):
|
||||
@ -61,6 +62,7 @@ ALLOWED_FIELDS = {
|
||||
"issue_tracker": str,
|
||||
"issueTracker": str,
|
||||
"forums": int,
|
||||
"video_url": str,
|
||||
}
|
||||
|
||||
ALIASES = {
|
||||
@ -128,8 +130,13 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
||||
if "media_license" in data:
|
||||
data["media_license"] = get_license(data["media_license"])
|
||||
|
||||
if "video_url" in data:
|
||||
data["video_url"] = clean_youtube_url(data["video_url"])
|
||||
if data["video_url"] is None:
|
||||
raise LogicError(400, lazy_gettext("Video URL is not a YouTube video URL"))
|
||||
|
||||
for key in ["name", "title", "short_desc", "desc", "type", "dev_state", "license", "media_license",
|
||||
"repo", "website", "issueTracker", "forums"]:
|
||||
"repo", "website", "issueTracker", "forums", "video_url"]:
|
||||
if key in data:
|
||||
setattr(package, key, data[key])
|
||||
|
||||
|
@ -389,6 +389,7 @@ class Package(db.Model):
|
||||
website = db.Column(db.String(200), nullable=True)
|
||||
issueTracker = db.Column(db.String(200), nullable=True)
|
||||
forums = db.Column(db.Integer, nullable=True)
|
||||
video_url = db.Column(db.String(200), nullable=True, default=None)
|
||||
|
||||
provides = db.relationship("MetaPackage", secondary=PackageProvides, order_by=db.asc("name"), back_populates="packages")
|
||||
|
||||
@ -527,6 +528,7 @@ class Package(db.Model):
|
||||
"website": self.website,
|
||||
"issue_tracker": self.issueTracker,
|
||||
"forums": self.forums,
|
||||
"video_url": self.video_url,
|
||||
|
||||
"tags": [x.name for x in self.tags],
|
||||
"content_warnings": [x.name for x in self.content_warnings],
|
||||
|
24
app/public/static/video_embed.js
Normal file
24
app/public/static/video_embed.js
Normal file
@ -0,0 +1,24 @@
|
||||
document.querySelectorAll(".video-embed").forEach(ele => {
|
||||
const url = new URL(ele.getAttribute("href"));
|
||||
|
||||
if (url.host == "www.youtube.com") {
|
||||
ele.addEventListener("click", () => {
|
||||
ele.parentNode.classList.add("d-block");
|
||||
ele.classList.add("embed-responsive");
|
||||
ele.classList.add("embed-responsive-16by9");
|
||||
ele.innerHTML = `
|
||||
<iframe title="YouTube video player" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>`;
|
||||
|
||||
const embedURL = new URL("https://www.youtube.com/");
|
||||
embedURL.pathname = "/embed/" + url.searchParams.get("v");
|
||||
embedURL.searchParams.set("autoplay", "1");
|
||||
|
||||
const iframe = ele.children[0];
|
||||
iframe.setAttribute("src", embedURL);
|
||||
});
|
||||
ele.removeAttribute("href");
|
||||
}
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
@import "components.scss";
|
||||
@import "packages.scss";
|
||||
@import "gallery.scss";
|
||||
@import "packagegrid.scss";
|
||||
@import "comments.scss";
|
||||
|
||||
|
84
app/scss/gallery.scss
Normal file
84
app/scss/gallery.scss
Normal file
@ -0,0 +1,84 @@
|
||||
.gallery {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 2em;
|
||||
overflow: auto hidden;
|
||||
|
||||
li, li a {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 5px;
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
position: relative;
|
||||
|
||||
&:hover img {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
height: 133px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.video-embed {
|
||||
min-width: 200px;
|
||||
min-height: 133px;
|
||||
background: #111;
|
||||
position: relative;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
cursor: pointer;
|
||||
|
||||
.fas {
|
||||
display: block;
|
||||
font-size: 200%;
|
||||
color: #f44;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #191919;
|
||||
|
||||
.fas {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.screenshot-add {
|
||||
display: block !important;
|
||||
width: 200px;
|
||||
height: 133px;
|
||||
background: #444;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
line-height: 133px !important;
|
||||
font-size: 80px;
|
||||
|
||||
&:hover {
|
||||
background: #555;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
@ -1,32 +1,3 @@
|
||||
.screenshot_list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 2em;
|
||||
|
||||
li, li a {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 5px;
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
height: 133px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-tr {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
@ -34,23 +5,6 @@
|
||||
color: #ccc !important;;
|
||||
}
|
||||
|
||||
.screenshot-add {
|
||||
display: block !important;
|
||||
width: 200px;
|
||||
height: 133px;
|
||||
background: #444;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
line-height: 133px !important;
|
||||
font-size: 80px;
|
||||
|
||||
&:hover {
|
||||
background: #555;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-row {
|
||||
vertical-align: middle;
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=32">
|
||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=33">
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
||||
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
|
||||
<link rel="icon" href="/favicon-128.png" sizes="128x128">
|
||||
|
@ -117,6 +117,7 @@
|
||||
pattern="[0-9]+",
|
||||
prefix="forum.minetest.net/viewtopic.php?t=",
|
||||
placeholder=_("Tip: paste in a forum topic URL")) }}
|
||||
{{ render_field(form.video_url, class_="pkg_meta", hint=_("Only supports YouTube, for now")) }}
|
||||
</fieldset>
|
||||
|
||||
<div class="pkg_meta mt-5">{{ render_submit_field(form.submit) }}</div>
|
||||
|
@ -78,6 +78,11 @@
|
||||
|
||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||
</form>
|
||||
|
||||
<h2>{{ _("Videos") }}</h2>
|
||||
<p>
|
||||
{{ _("You can set a video on the Edit Details page") }}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block scriptextra %}
|
||||
|
@ -17,6 +17,10 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scriptextra %}
|
||||
<script src="/static/video_embed.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_license(license) %}
|
||||
{% if license.url %}
|
||||
<a href="{{ license.url }}">{{ license.name }}</a>
|
||||
@ -89,19 +93,19 @@
|
||||
<div class="container">
|
||||
<div class="btn-group float-right mb-4">
|
||||
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
|
||||
<a class="btn btn-primary" href="{{ package.getURL("packages.create_edit") }}">
|
||||
<a class="btn btn-primary" href="{{ package.getURL('packages.create_edit') }}">
|
||||
<i class="fas fa-pen mr-1"></i>
|
||||
{{ _("Edit") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
||||
<a class="btn btn-primary" href="{{ package.getURL("packages.create_release") }}">
|
||||
<a class="btn btn-primary" href="{{ package.getURL('packages.create_release') }}">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
{{ _("Release") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if package.checkPerm(current_user, "DELETE_PACKAGE") or package.checkPerm(current_user, "UNAPPROVE_PACKAGE") %}
|
||||
<a class="btn btn-danger" href="{{ package.getURL("packages.remove") }}">
|
||||
<a class="btn btn-danger" href="{{ package.getURL('packages.remove') }}">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
{{ _("Remove") }}
|
||||
</a>
|
||||
@ -236,33 +240,44 @@
|
||||
<div class="row">
|
||||
<div class="col-md-9" style="padding-right: 45px;">
|
||||
{% set screenshots = package.screenshots.all() %}
|
||||
{% if screenshots or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
||||
{% if package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
||||
<a href="{{ package.getURL("packages.screenshots") }}" class="btn btn-primary float-right">
|
||||
<i class="fas fa-images mr-1"></i>
|
||||
{{ _("Edit") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="screenshot_list">
|
||||
{% for ss in screenshots %}
|
||||
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
||||
<li>
|
||||
<a href="{{ ss.url }}" class="position-relative">
|
||||
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
|
||||
{% if not ss.approved %}
|
||||
<span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
||||
<a href="{{ package.getURL('packages.screenshots') }}" class="btn btn-primary float-right">
|
||||
<i class="fas fa-images mr-1"></i>
|
||||
{{ _("Edit") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if screenshots or package.checkPerm(current_user, "ADD_SCREENSHOTS") or package.video_url %}
|
||||
<ul class="gallery">
|
||||
{% if package.video_url %}
|
||||
<li>
|
||||
<a href="{{ package.getURL("packages.create_screenshot") }}">
|
||||
<i class="fas fa-plus screenshot-add"></i>
|
||||
<a href="{{ package.video_url }}" class="video-embed" title="{{ _('YouTube video embed') }}">
|
||||
<i class="fas fa-play"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if screenshots or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
||||
{% for ss in screenshots %}
|
||||
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
||||
<li>
|
||||
<a href="{{ ss.url }}" class="gallery-image">
|
||||
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
|
||||
{% if not ss.approved %}
|
||||
<span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li>
|
||||
<a href="{{ package.getURL('packages.create_screenshot') }}">
|
||||
<i class="fas fa-plus screenshot-add"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
13
app/tests/unit/test_url.py
Normal file
13
app/tests/unit/test_url.py
Normal file
@ -0,0 +1,13 @@
|
||||
from app.utils.url import clean_youtube_url
|
||||
|
||||
|
||||
def test_clean_youtube_url():
|
||||
assert clean_youtube_url(
|
||||
"https://www.youtube.com/watch?v=AABBCC") == "https://www.youtube.com/watch?v=AABBCC"
|
||||
assert clean_youtube_url(
|
||||
"https://www.youtube.com/watch?v=boGcB4H5-WA&other=1") == "https://www.youtube.com/watch?v=boGcB4H5-WA"
|
||||
assert clean_youtube_url("https://www.youtube.com/watch?kk=boGcB4H5-WA&other=1") is None
|
||||
assert clean_youtube_url("https://www.bob.com/watch?v=AABBCC") is None
|
||||
|
||||
assert clean_youtube_url("https://youtu.be/boGcB4H5-WA") == "https://www.youtube.com/watch?v=boGcB4H5-WA"
|
||||
assert clean_youtube_url("https://youtu.be/boGcB4H5-WA?this=1") == "https://www.youtube.com/watch?v=boGcB4H5-WA"
|
@ -14,13 +14,12 @@
|
||||
# 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 re
|
||||
import secrets
|
||||
|
||||
from .flask import *
|
||||
from .models import *
|
||||
from .user import *
|
||||
import re
|
||||
|
||||
|
||||
YESES = ["yes", "true", "1", "on"]
|
||||
|
||||
|
46
app/utils/url.py
Normal file
46
app/utils/url.py
Normal file
@ -0,0 +1,46 @@
|
||||
# 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 urllib.parse as urlparse
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
|
||||
def url_set_query(url: str, params: Dict[str, str]) -> str:
|
||||
url_parts = list(urlparse.urlparse(url))
|
||||
query = dict(urlparse.parse_qsl(url_parts[4]))
|
||||
query.update(params)
|
||||
|
||||
url_parts[4] = urlparse.urlencode(query)
|
||||
return urlparse.urlunparse(url_parts)
|
||||
|
||||
|
||||
def url_get_query(parsed_url: urlparse.ParseResult) -> Dict[str, List[str]]:
|
||||
return urlparse.parse_qs(parsed_url.query)
|
||||
|
||||
|
||||
def clean_youtube_url(url: str) -> Optional[str]:
|
||||
parsed = urlparse.urlparse(url)
|
||||
print(parsed)
|
||||
if (parsed.netloc == "www.youtube.com" or parsed.netloc == "youtube.com") and parsed.path == "/watch":
|
||||
print(url_get_query(parsed))
|
||||
video_id = url_get_query(parsed).get("v", [None])[0]
|
||||
if video_id:
|
||||
return url_set_query("https://www.youtube.com/watch", {"v": video_id})
|
||||
|
||||
elif parsed.netloc == "youtu.be":
|
||||
return url_set_query("https://www.youtube.com/watch", {"v": parsed.path[1:]})
|
||||
|
||||
return None
|
25
migrations/versions/011e42c52d21_.py
Normal file
25
migrations/versions/011e42c52d21_.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 011e42c52d21
|
||||
Revises: 6e57b2b4dcdf
|
||||
Create Date: 2022-01-25 18:48:46.367409
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '011e42c52d21'
|
||||
down_revision = '6e57b2b4dcdf'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('package', sa.Column('video_url', sa.String(length=200), nullable=True))
|
||||
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('package', 'video_url')
|
Loading…
Reference in New Issue
Block a user