mirror of
https://github.com/YunoHost/apps.git
synced 2024-09-03 20:06:07 +02:00
appstore: implement star logic, at least on catalog
This commit is contained in:
parent
352aeac146
commit
37330d3d07
6 changed files with 115 additions and 21 deletions
1
store/.gitignore
vendored
1
store/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
config.toml
|
||||
.stars
|
||||
|
|
56
store/app.py
56
store/app.py
|
@ -1,3 +1,4 @@
|
|||
import subprocess
|
||||
import pycmarkgfm
|
||||
import time
|
||||
import re
|
||||
|
@ -30,11 +31,13 @@ except Exception as e:
|
|||
mandatory_config_keys = [
|
||||
"DISCOURSE_SSO_SECRET",
|
||||
"DISCOURSE_SSO_ENDPOINT",
|
||||
"COOKIE_SECRET",
|
||||
"CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE",
|
||||
"GITHUB_LOGIN",
|
||||
"GITHUB_TOKEN",
|
||||
"GITHUB_EMAIL",
|
||||
"APPS_CACHE",
|
||||
"STARS_DB_FOLDER",
|
||||
]
|
||||
|
||||
for key in mandatory_config_keys:
|
||||
|
@ -70,8 +73,23 @@ for id_, category in catalog['categories'].items():
|
|||
wishlist = toml.load(open("../wishlist.toml"))
|
||||
|
||||
# This is the secret key used for session signing
|
||||
app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)])
|
||||
app.secret_key = config["COOKIE_SECRET"]
|
||||
|
||||
def get_stars():
|
||||
checksum = subprocess.check_output("find . -type f -printf '%T@,' | md5sum", shell=True).decode().split()[0]
|
||||
if get_stars.cache_checksum != checksum:
|
||||
stars = {}
|
||||
for folder, _, files in os.walk(config["STARS_DB_FOLDER"]):
|
||||
app_id = folder.split("/")[-1]
|
||||
if not app_id:
|
||||
continue
|
||||
stars[app_id] = set(files)
|
||||
get_stars.cache_stars = stars
|
||||
get_stars.cache_checksum = checksum
|
||||
|
||||
return get_stars.cache_stars
|
||||
get_stars.cache_checksum = None
|
||||
get_stars()
|
||||
|
||||
def human_to_binary(size: str) -> int:
|
||||
symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y")
|
||||
|
@ -110,6 +128,7 @@ def login_using_discourse():
|
|||
|
||||
session.clear()
|
||||
session["nonce"] = nonce
|
||||
print(f"DEBUG: none = {nonce}")
|
||||
|
||||
return redirect(url)
|
||||
|
||||
|
@ -118,6 +137,8 @@ def login_using_discourse():
|
|||
def sso_login_callback():
|
||||
response = base64.b64decode(request.args['sso'].encode()).decode()
|
||||
user_data = urllib.parse.parse_qs(response)
|
||||
print("DEBUG: nonce from url args " + user_data['nonce'][0])
|
||||
print("DEBUG: nonce from session args " + session.get("nonce"))
|
||||
if user_data['nonce'][0] != session.get("nonce"):
|
||||
return "Invalid nonce", 401
|
||||
else:
|
||||
|
@ -143,7 +164,7 @@ def index():
|
|||
|
||||
@app.route('/catalog')
|
||||
def browse_catalog():
|
||||
return render_template("catalog.html", init_sort=request.args.get("sort"), init_search=request.args.get("search"), init_category=request.args.get("category"), user=session.get('user', {}), catalog=catalog, timestamp_now=int(time.time()))
|
||||
return render_template("catalog.html", init_sort=request.args.get("sort"), init_search=request.args.get("search"), init_category=request.args.get("category"), init_starsonly=request.args.get("starsonly"), user=session.get('user', {}), catalog=catalog, timestamp_now=int(time.time()), stars=get_stars())
|
||||
|
||||
|
||||
@app.route('/app/<app_id>')
|
||||
|
@ -194,12 +215,39 @@ def app_info(app_id):
|
|||
ram_build_requirement = infos["manifest"]["integration"]["ram"]["build"]
|
||||
infos["manifest"]["integration"]["ram"]["build_binary"] = human_to_binary(ram_build_requirement)
|
||||
|
||||
return render_template("app.html", user=session.get('user', {}), app_id=app_id, infos=infos, catalog=catalog)
|
||||
return render_template("app.html", user=session.get('user', {}), app_id=app_id, infos=infos, catalog=catalog, stars=get_stars())
|
||||
|
||||
|
||||
@app.route('/app/<app_id>/<action>')
|
||||
def star_app(app_id, action):
|
||||
assert action in ["star", "unstar"]
|
||||
if app_id not in catalog["apps"] and app_id not in wishlist:
|
||||
return f"App {app_id} not found", 404
|
||||
if not session.get('user', {}):
|
||||
return f"You must be logged in to be able to star an app", 401
|
||||
|
||||
app_star_folder = os.path.join(config["STARS_DB_FOLDER"], app_id)
|
||||
app_star_for_this_user = os.path.join(config["STARS_DB_FOLDER"], app_id, session.get('user', {})["id"])
|
||||
|
||||
if not os.path.exists(app_star_folder):
|
||||
os.mkdir(app_star_folder)
|
||||
|
||||
if action == "star":
|
||||
open(app_star_for_this_user, "w").write("")
|
||||
elif action == "unstar":
|
||||
try:
|
||||
os.remove(app_star_for_this_user)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if app_id in catalog["apps"]:
|
||||
return redirect(f"/app/{app_id}")
|
||||
else:
|
||||
return redirect("/wishlist")
|
||||
|
||||
@app.route('/wishlist')
|
||||
def browse_wishlist():
|
||||
return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist)
|
||||
return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist, stars=get_stars())
|
||||
|
||||
|
||||
@app.route('/wishlist/add', methods=['GET', 'POST'])
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
COOKIE_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
# This secret is configured in Discourse
|
||||
DISCOURSE_SSO_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
DISCOURSE_SSO_ENDPOINT = "https://forum.yunohost.org/session/sso_provider"
|
||||
CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = "http://localhost:5000/sso_login_callback"
|
||||
DEBUG = false
|
||||
GITHUB_LOGIN = "yunohost-bot"
|
||||
GITHUB_EMAIL = "yunohost [at] yunohost.org" # Replace the [at] by actual @
|
||||
GITHUB_TOKEN = "superSecretToken"
|
||||
APPS_CACHE = "../.apps_cache/"
|
||||
STARS_DB_FOLDER = ".stars/"
|
||||
|
|
|
@ -35,12 +35,29 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="mt-2">
|
||||
<a
|
||||
href="#"
|
||||
class="mr-3 inline-block group btn border text-violet-600 border-violet-500 hover:bg-violet-500 hover:text-white"
|
||||
>
|
||||
123 <i class="fa fa-star-o inline-block group-hover:hidden" aria-hidden="true"></i>
|
||||
<i class="fa fa-star hidden group-hover:inline-block" aria-hidden="true"></i>
|
||||
{% set this_app_stars = stars.get(app_id, {})|length %}
|
||||
{% if user %}
|
||||
{% set user_starred_this_app = user['id'] in stars.get(app_id, {}) %}
|
||||
{% else %}
|
||||
{% set user_starred_this_app = False %}
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
href="{{ url_for('star_app', app_id=app_id, action="unstar" if user_starred_this_app else "star") }}"
|
||||
role="button"
|
||||
class="mr-3 inline-block group btn border text-violet-600 border-violet-500 {% if user %}hover:bg-violet-500 hover:text-white{% endif %}"
|
||||
>
|
||||
{% if not user_starred_this_app %}
|
||||
<span class="inline-block {% if user %}group-hover:hidden{% endif %}">{{ this_app_stars }}</span>
|
||||
<span class="hidden {% if user %}group-hover:inline-block{% endif %}">{{ this_app_stars+1 }}</span>
|
||||
<i class="fa fa-star-o inline-block {% if user %}group-hover:hidden{% endif %}" aria-hidden="true"></i>
|
||||
<i class="fa fa-star hidden {% if user %}group-hover:inline-block{% endif %}" aria-hidden="true"></i>
|
||||
{% else %}
|
||||
<span class="inline-block group-hover:hidden">{{ this_app_stars }}</span>
|
||||
<span class="hidden group-hover:inline-block">{{ this_app_stars-1 }}</span>
|
||||
<i class="fa fa-star inline-block group-hover:hidden" aria-hidden="true"></i>
|
||||
<i class="fa fa-star-o hidden group-hover:inline-block" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if infos["manifest"]["upstream"]["demo"] %}
|
||||
<a
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
{% set locale = 'en' %}
|
||||
|
||||
{% macro appCard(app, infos, timestamp_now, catalog) -%}
|
||||
|
||||
{% set this_app_stars = stars.get(app, {})|length %}
|
||||
{% if user %}
|
||||
{% set user_starred_this_app = user['id'] in stars.get(app, {}) %}
|
||||
{% else %}
|
||||
{% set user_starred_this_app = False %}
|
||||
{% endif %}
|
||||
|
||||
<div class="search-entry"
|
||||
data-appid="{{ app }}"
|
||||
data-stars="{{ this_app_stars }}"
|
||||
data-starred="{{ user_starred_this_app }}"
|
||||
data-addedincatalog="{{ ((timestamp_now - infos['added_in_catalog']) / 3600 / 24) | int }}"
|
||||
data-category="{%- if infos['category'] -%}{{ infos['category'] }}{%- endif -%}"
|
||||
>
|
||||
|
@ -36,9 +46,9 @@
|
|||
{% elif infos['level'] == 8 %}
|
||||
<i class="fa fa-diamond text-teal-500 py-0.5" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
<span class="group text-violet-500 hover:text-white hover:bg-violet-500 rounded-md px-1 py-0.5">
|
||||
123 <i class="fa fa-star-o inline-block group-hover:hidden" aria-hidden="true"></i>
|
||||
<i class="fa fa-star hidden group-hover:inline-block" aria-hidden="true"></i>
|
||||
<span class="inline-block group rounded-md text-xs text-violet-500 px-1 py-0.5">
|
||||
<span class="inline-block">{{ this_app_stars }}</span>
|
||||
<i class="fa {% if not user_starred_this_app %}fa-star-o{% else %}fa-star{% endif %} inline-block" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -113,8 +123,8 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="inline-block flex items-center px-2">
|
||||
<label for="onlyfav" class="inline-block relative mr-2 h-4 w-7 cursor-pointer">
|
||||
<input type="checkbox" id="onlyfav" class="peer sr-only" />
|
||||
<label for="starsonly" class="inline-block relative mr-2 h-4 w-7 cursor-pointer">
|
||||
<input type="checkbox" id="starsonly" class="peer sr-only" {% if user and init_starsonly %}checked{% endif %} />
|
||||
|
||||
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500">
|
||||
</span>
|
||||
|
@ -122,7 +132,7 @@
|
|||
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
|
||||
</span>
|
||||
</label>
|
||||
Show only your bookmarks
|
||||
Show only apps you starred
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -151,7 +161,7 @@
|
|||
Applications currently broken or low-quality
|
||||
</h2>
|
||||
<p class="text-sm">
|
||||
There are apps which failed our automatic tests.<br/>
|
||||
These are apps which failed our automatic tests.<br/>
|
||||
This is usually a temporary situation which requires packagers to fix something in the app.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -172,6 +182,7 @@
|
|||
let searchInput = document.getElementById('search');
|
||||
let selectCategory = document.getElementById('selectcategory');
|
||||
let selectSort = document.getElementById('selectsort');
|
||||
let toggleStarsonly = document.getElementById('starsonly');
|
||||
|
||||
function liveSearch() {
|
||||
// Locate the card elements
|
||||
|
@ -179,12 +190,14 @@
|
|||
// Locate the search input
|
||||
let search_query = searchInput.value.trim().toLowerCase();
|
||||
let selectedCategory = selectCategory.value.trim();
|
||||
let starsOnly = toggleStarsonly.checked;
|
||||
let at_least_one_match = false;
|
||||
// Loop through the entries
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
// If the text is within the card and the text matches the search query
|
||||
if ((entries[i].textContent.toLowerCase().includes(search_query))
|
||||
&& (! selectedCategory || (entries[i].dataset.category == selectedCategory)))
|
||||
&& (! selectedCategory || (entries[i].dataset.category == selectedCategory))
|
||||
&& (! starsOnly || (entries[i].dataset.starred == "True")))
|
||||
{
|
||||
// ...remove the `.is-hidden` class.
|
||||
entries[i].classList.remove("hidden");
|
||||
|
@ -220,6 +233,11 @@
|
|||
return a.dataset.addedincatalog - b.dataset.addedincatalog;
|
||||
});
|
||||
}
|
||||
else if (sortBy === "popularity") {
|
||||
toSort.sort(function(a, b) {
|
||||
return a.dataset.stars < b.dataset.stars;
|
||||
});
|
||||
}
|
||||
else if (sortBy === "") {
|
||||
toSort.sort(function(a, b) {
|
||||
return a.dataset.appid > b.dataset.appid;
|
||||
|
@ -239,12 +257,14 @@
|
|||
let search_query = searchInput.value.trim();
|
||||
let category = selectCategory.value.trim();
|
||||
let sortBy = selectSort.value.trim();
|
||||
let starsOnly = toggleStarsonly.checked;
|
||||
|
||||
if ('URLSearchParams' in window) {
|
||||
var queryArgs = new URLSearchParams(window.location.search)
|
||||
if (search_query) { queryArgs.set("search", search_query) } else { queryArgs.delete("search"); };
|
||||
if (category) { queryArgs.set("category", category) } else { queryArgs.delete("category"); };
|
||||
if (sortBy) { queryArgs.set("sort", sortBy) } else { queryArgs.delete("sortBy"); };
|
||||
if (starsOnly) { queryArgs.set("starsonly", true) } else { queryArgs.delete("starsonly"); };
|
||||
|
||||
let newUrl;
|
||||
if (queryArgs.toString())
|
||||
|
@ -278,6 +298,11 @@
|
|||
liveSort("catalogLowQuality");
|
||||
});
|
||||
|
||||
toggleStarsonly.addEventListener('change', () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(liveSearch, typeInterval);
|
||||
});
|
||||
|
||||
liveSearch();
|
||||
liveSort("catalogGoodQuality");
|
||||
liveSort("catalogLowQuality");
|
||||
|
|
|
@ -85,8 +85,9 @@
|
|||
href="#"
|
||||
class="inline-block group btn-sm border text-violet-600 border-violet-500 hover:bg-violet-500 hover:text-white"
|
||||
>
|
||||
123 <i class="fa fa-star-o inline-block group-hover:hidden" aria-hidden="true"></i>
|
||||
<i class="fa fa-star hidden group-hover:inline-block" aria-hidden="true"></i>
|
||||
{{ stars.get(app, {})|length }}
|
||||
<i class="fa fa-star-o inline-block group-hover:hidden" aria-hidden="true"></i>
|
||||
<i class="fa fa-star hidden group-hover:inline-block" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
Loading…
Add table
Reference in a new issue