UI: Improve multi OS display #205

Merged
Oleg-Komarov merged 40 commits from ui/multi-os into main 2024-07-16 07:24:07 +02:00
10 changed files with 257 additions and 57 deletions

View File

@ -100,13 +100,120 @@
const btnInstallAction = document.querySelector('.js-btn-install-action'); const btnInstallAction = document.querySelector('.js-btn-install-action');
const btnInstallGroup = document.querySelector('.js-btn-install-group'); const btnInstallGroup = document.querySelector('.js-btn-install-group');
const dropdownAllVersionsItemDot = document.querySelectorAll('.js-dropdown-all-versions-item-dot');
const dropdownAllVersionsItemIcon = document.querySelectorAll('.js-dropdown-all-versions-item-icon');
const dropdownAllVersionsWrapper = document.querySelector('.js-dropdown-all-versions-wrapper');
// Create variables multiple // Create variables multiple
const btnInstallActionItem = document.querySelectorAll('.js-btn-install-action-item'); const btnInstallActionItem = document.querySelectorAll('.js-btn-install-action-item');
// Create function isArchARM
function isArchARM() {
// Check if userAgentData and getHighEntropyValues are supported
if (navigator.userAgentData && navigator.userAgentData.getHighEntropyValues) {
navigator.userAgentData
.getHighEntropyValues(["architecture"])
.then((values) => {
// Extract the architecture value
const arch = values.architecture;
// Check if the architecture is ARM
if (arch.toLowerCase().includes('arm')) {
return true;
} else {
return false;
}
});
} else {
// Fallback for browsers that do not support userAgentData or getHighEntropyValues
return false;
}
}
if (btnInstall == null) { if (btnInstall == null) {
return; return;
} }
// Hide dropdownAllVersionsWrapper by default
dropdownAllVersionsWrapper.classList.add('d-none');
// Show multi OS install buttons based on active platform
if (btnInstallActionItem.length > 1) {
// Show dropdownAllVersionsWrapper
dropdownAllVersionsWrapper.classList.remove('d-none');
// Get active platform
/*
The navigator objects' 'platform' proporties are arbitrary, and specific to the browser.
For a light match, we can use the first 3 characters of the platform string.
*/
const activePlatform = navigator.platform.toLowerCase();
const activePlatformPrefix = activePlatform.substring(0, 3);
btnInstallActionItem.forEach(function(item) {
// Hide all items by default
item.classList.add('d-none');
// Hide dropdownAllVersionsItemDot by default
dropdownAllVersionsItemDot.forEach(function(item) {
item.classList.add('d-none');
});
// Style dropdownAllVersionsItemIcon by default
dropdownAllVersionsItemIcon.forEach(function(item) {
item.classList.add('text-muted');
});
// Get item platform
const itemPlatform = item.getAttribute('data-platform');
const itemPlatformBase = itemPlatform.split('-')[0];
const itemPlatformBasePrefix = itemPlatformBase.substring(0, 3);
// Show button if item platform matches active platform
if (itemPlatformBasePrefix.startsWith(activePlatformPrefix)) {
item.classList.add('btn-install-action-item-active');
item.classList.remove('d-none');
// TODO: WIP show only relevant item for active macOS architecture, if architecture detection reliably works
if (activePlatformPrefix.startsWith('mac')) {
if (isArchARM()) {
} else {
}
}
}
});
// TODO: refactor and DRY code
dropdownAllVersionsItemDot.forEach(function(item) {
// TODO: create named function to not repeat this (optional)
// Get item platform
const itemPlatform = item.getAttribute('data-platform');
const itemPlatformBase = itemPlatform.split('-')[0];
const itemPlatformBasePrefix = itemPlatformBase.substring(0, 3);
// Show dropdownAllVersionsItemDot if item platform matches active platform
if (itemPlatformBasePrefix.startsWith(activePlatformPrefix)) {
item.classList.remove('d-none');
}
});
dropdownAllVersionsItemIcon.forEach(function(item) {
// Get item platform
const itemPlatform = item.getAttribute('data-platform');
const itemPlatformBase = itemPlatform.split('-')[0];
const itemPlatformBasePrefix = itemPlatformBase.substring(0, 3);
// Show dropdownAllVersionsItemIcon if item platform matches active platform
if (itemPlatformBasePrefix.startsWith(activePlatformPrefix)) {
item.classList.remove('text-muted');
}
});
}
// Show btnInstallAction // Show btnInstallAction
btnInstall.addEventListener('click', function() { btnInstall.addEventListener('click', function() {
// Hide btnInstallGroup // Hide btnInstallGroup

View File

@ -167,8 +167,9 @@
.btn-install-action-item .btn-install-action-item
+margin(3, bottom) +margin(3, bottom)
&:last-child .btn-install-action-item-active ~ .btn-install-action-item-active
+margin(0, bottom) border-top: thin solid var(--border-color)
+padding(3, top)
.btn-install-drag, .btn-install-drag,
.btn-install-drag:active .btn-install-drag:active
@ -474,3 +475,11 @@ a
.featured-image-preview .featured-image-preview
width: 16rem width: 16rem
.dropdown-all-versions-item-dot
background-color: var(--color-accent)
border-radius: 50%
height: var(--spacer-1)
position: absolute
transform: translateX(-1.2rem)
width: var(--spacer-1)

View File

@ -35,6 +35,9 @@
.mw-#{$i} .mw-#{$i}
min-width: var(--spacer-#{$i}) min-width: var(--spacer-#{$i})
.list-style-none
list-style: none
.opacity-50 .opacity-50
opacity: 0.5 opacity: 0.5

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.11 on 2024-05-14 11:06
from django.db import migrations, models
def update_platforms(apps, schema_editor):
lookup = {
"windows-x64": "Windows",
"windows-arm64": "Windows Arm",
"macos-x64": "macOS Intel",
"macos-arm64": "macOS Apple Silicon",
"linux-x64": "Linux",
}
Platform = apps.get_model('extensions', 'Platform')
for ob in Platform.objects.all():
ob.name = lookup.get(ob.slug, ob.name)
ob.save()
class Migration(migrations.Migration):
dependencies = [
('extensions', '0041_remove_version_file'),
]
operations = [
migrations.RunPython(update_platforms),
]

View File

@ -64,6 +64,20 @@ class Platform(CreatedModifiedMixin, models.Model):
def get_by_slug(cls, slug: str): def get_by_slug(cls, slug: str):
return cls.objects.filter(slug=slug).first() return cls.objects.filter(slug=slug).first()
@property
def name_first_word(self):
"""Used for presentation in download_list."""
return self.name.split(None, 1)[0]
@property
def name_rest(self):
"""Used for presentation in download_list."""
parts = self.name.split(None, 1)
if len(parts) > 1:
return parts[1]
else:
return ''
class ExtensionManager(models.Manager): class ExtensionManager(models.Manager):
@property @property
@ -706,6 +720,18 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
return file return file
return None return None
def get_supported_platforms(self):
supported_platforms = {}
for platform in self.platforms.all():
architectures = supported_platforms.get(platform.name_first_word, [])
architectures.append(platform.name_rest)
supported_platforms[platform.name_first_word] = architectures
result = []
for platform, architectures in sorted(supported_platforms.items()):
item = {'name': platform, 'architectures': sorted(architectures)}
result.append(item)
return result
@property @property
def is_listed(self): def is_listed(self):
# To be public, at least one version file must have a public status. # To be public, at least one version file must have a public status.
@ -785,7 +811,7 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
'url': self.get_download_url(file), 'url': self.get_download_url(file),
} }
] ]
platform2file = {} platform_slug2file = {}
for file in files: for file in files:
platforms = file.get_platforms() platforms = file.get_platforms()
if not platforms: if not platforms:
@ -793,16 +819,17 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
f'data error: Version pk={self.pk} has multiple files, but File pk={file.pk} ' f'data error: Version pk={self.pk} has multiple files, but File pk={file.pk} '
f'is not platform-specific' f'is not platform-specific'
) )
for platform in platforms: for platform_slug in platforms:
platform2file[platform] = file platform_slug2file[platform_slug] = file
all_platforms_by_slug = {p.slug: p for p in Platform.objects.all()}
return [ return [
{ {
'name': self.get_download_name(file), 'name': self.get_download_name(file),
'platform': p, 'platform': all_platforms_by_slug.get(platform_slug),
'size': file.size_bytes, 'size': file.size_bytes,
'url': self.get_download_url(file), 'url': self.get_download_url(file),
} }
for p, file in platform2file.items() for platform_slug, file in sorted(platform_slug2file.items(), reverse=True)
] ]
def get_build_list(self) -> List[dict]: def get_build_list(self) -> List[dict]:

View File

@ -6,7 +6,7 @@
{% has_maintainer extension as is_maintainer %} {% has_maintainer extension as is_maintainer %}
{% with latest=extension.latest_version %} {% with latest=extension.latest_version %}
<div class="hero extension-detail"> <div class="hero extension-detail position-relative z-2">
<div class="container"> <div class="container">
<div class="hero-content"> <div class="hero-content">
{% block hero_breadcrumbs %} {% block hero_breadcrumbs %}

View File

@ -67,16 +67,13 @@
</a> </a>
</dd> </dd>
</div> </div>
<div class="dl-col">
<dt>{% trans 'Size' %}</dt> {% if version.has_single_file %}
{% if version.has_single_file %} <div class="dl-col">
<dd>{{ version.files.first.size_bytes|filesizeformat }}</dd> <dt>{% trans 'Size' %}</dt>
{% else %} <dd>{{ version.files.first.size_bytes|filesizeformat }}</dd>
{% for file in version.files.all %} </div>
<dd>{{ file.size_bytes|filesizeformat }} ({{ file.get_platforms|join:", " }})</dd> {% endif %}
{% endfor %}
{% endif %}
</div>
</div> </div>
<div class="dl-row"> <div class="dl-row">

View File

@ -1,16 +1,18 @@
{% load i18n %} {% load i18n %}
{% if version.platforms.all %} {% with supported_platforms=version.get_supported_platforms %}
{% if supported_platforms %}
<div class="dl-row"> <div class="dl-row">
<div class="dl-col"> <div class="dl-col">
<dt>{% trans "Supported Platforms" %}</dt> <dt>{% trans "Supported Platforms" %}</dt>
<dd> <dd>
<ul class="mb-0 ps-3"> <ul class="list-style-none mb-0 ps-0">
{% for p in version.platforms.all %} {% for item in supported_platforms|dictsortreversed:"name" %}
<li>{{p.name}}</li> <li><i class="i-{{ item.name|lower }}"></i> {{ item.name }} <span class="text-muted">{{ item.architectures|join:', ' }}</span></li>
{% endfor %} {% endfor %}
</ul> </ul>
</dd> </dd>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endwith %}

View File

@ -110,7 +110,7 @@
{# Sidebar #} {# Sidebar #}
<div class="col-md-4"> <div class="col-md-4">
<aside class="is-sticky pt-2"> <aside class="is-sticky pt-2 z-1">
{# Info Summary #} {# Info Summary #}
{% block extension_summary_sidebar %} {% block extension_summary_sidebar %}
@ -185,23 +185,13 @@
<dt>{% trans 'Downloads' %}</dt> <dt>{% trans 'Downloads' %}</dt>
<dd>{{ extension.download_count }}</dd> <dd>{{ extension.download_count }}</dd>
</div> </div>
<div class="dl-col">
<dt>{% trans 'Size' %}</dt> {% if latest.has_single_file %}
{% if latest.has_single_file %} <div class="dl-col">
<dt>{% trans 'Size' %}</dt>
<dd>{{ latest.files.first.size_bytes|filesizeformat }}</dd> <dd>{{ latest.files.first.size_bytes|filesizeformat }}</dd>
{% else %} </div>
<dd> {% endif %}
<ul class="ps-3">
{% for file in latest.files.all %}
<li class="lh-sm">
{{ file.size_bytes|filesizeformat }}<br>
<span class="fs-sm text-muted">({{ file.get_platforms|join:", " }})</span>
</li>
{% endfor %}
</ul>
</dd>
{% endif %}
</div>
</div> </div>
<div class="dl-row"> <div class="dl-row">
@ -272,12 +262,27 @@
</div> </div>
<div class="fade js-btn-install-action"> <div class="fade js-btn-install-action">
{% for download_item in download_list %} {% for download_item in download_list %}
<div class="btn-install-action-item js-btn-install-action-item" data-install-url="{{ request.scheme }}://{{ request.get_host }}{{ download_item.url }}" download="{{ download_item.name }}"> <div class="btn-install-action-item js-btn-install-action-item" data-install-url="{{ request.scheme }}://{{ request.get_host }}{{ download_item.url }}" data-platform="{{ download_item.platform.slug }}" download="{{ download_item.name }}">
<div class="btn-install-drag-group js-btn-install-drag-group mb-2 rounded"> <div class="btn-install-drag-group js-btn-install-drag-group mb-2 rounded">
<button class="btn btn-flex btn-primary btn-install-drag cursor-move js-btn-install-drag w-100" draggable="true"> <button class="btn btn-flex btn-primary btn-install-drag cursor-move js-btn-install-drag mb-2 w-100" draggable="true">
<i class="i-move"></i> <i class="i-move"></i>
<span>{% trans 'Drag and Drop into Blender' %} <span class="fs-sm">{{ download_item.platform }}</span></span> <span>{% trans 'Drag and Drop into Blender' %}
</span>
</button> </button>
<div class="text-center">
{% if download_list|length > 1 %}
<strong>
{% with platform=download_item.platform %}
{{ platform.name_first_word }}
{% if platform.name_rest %}
<span class="fw-normal"> - {{ platform.name_rest }}</span>
{% endif %}
{% endwith %}
</strong>
{{ download_item.size|filesizeformat }}
{% endif %}
</div>
</div> </div>
<small class="d-block text-center w-100"> <small class="d-block text-center w-100">
{# TODO @front-end: Replace URL of the manual /dev/ with /latest/. #} {# TODO @front-end: Replace URL of the manual /dev/ with /latest/. #}
@ -286,6 +291,27 @@
</small> </small>
</div> </div>
{% endfor %} {% endfor %}
<div class="dropdown js-dropdown-all-versions-wrapper">
<button class="btn btn-link dropdown-toggle js-dropdown-toggle w-100" data-toggle-menu-id="js-dropdown-all-versions">
</i> {% trans 'Other versions' %}<i class="i-chevron-down"></i>
</button>
<ul class="dropdown-menu js-dropdown-menu w-100" id="js-dropdown-all-versions">
{% for download_item in download_list %}
<li>
<a class="align-items-center d-flex dropdown-item justify-content-between" download="{{ download_item.name }}" href="{{ request.scheme }}://{{ request.get_host }}{{ download_item.url }}">
{% with platform=download_item.platform %}
<span class="dropdown-all-versions-item-dot js-dropdown-all-versions-item-dot" data-platform="{{ platform.name_first_word|lower|slice:"3" }}"></span>
<span>
<i class="i-{{ platform.name_first_word|lower }} js-dropdown-all-versions-item-icon me-0" data-platform="{{ platform.name_first_word|lower|slice:"3" }}"></i> {{ platform.name_first_word }} <span class="text-muted">{{ platform.name_rest}}</span>
</span>
{% endwith %}
<span class="text-muted">{{ download_item.size|filesizeformat }}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
</div> </div>
{# If JavaScript is disabled. #} {# If JavaScript is disabled. #}
@ -294,7 +320,7 @@
<div class="btn-col text-center"> <div class="btn-col text-center">
{% for download_item in download_list %} {% for download_item in download_list %}
<a class="btn btn-flex btn-accent" href="{{ request.scheme }}://{{ request.get_host }}{{ download_item.url }}" download="{{ download_item.name }}"> <a class="btn btn-flex btn-accent" href="{{ request.scheme }}://{{ request.get_host }}{{ download_item.url }}" download="{{ download_item.name }}">
<i class="i-download"></i><span>{% trans 'Download' %} {{ extension.get_type_display }} {{ download_item.platform }}</span> <i class="i-download"></i><span>{% trans 'Download' %} {{ extension.get_type_display }} {{ download_item.platform.slug }}</span>
</a> </a>
<small class="mt-3">...and <a class="text-underline text-primary text-center" href="https://docs.blender.org/manual/en/dev/editors/preferences/extensions.html#install" target="_blank">Install from Disk</a></small> <small class="mt-3">...and <a class="text-underline text-primary text-center" href="https://docs.blender.org/manual/en/dev/editors/preferences/extensions.html#install" target="_blank">Install from Disk</a></small>
{% endfor %} {% endfor %}

View File

@ -75,16 +75,12 @@
<dt>Downloads</dt> <dt>Downloads</dt>
<dd>{{ version.download_count }}</dd> <dd>{{ version.download_count }}</dd>
</div> </div>
<div class="dl-col"> {% if version.has_single_file %}
<dt>Size</dt> <div class="dl-col">
{% if version.has_single_file %} <dt>Size</dt>
<dd>{{ version.files.first.size_bytes|filesizeformat }}</dd> <dd>{{ version.files.first.size_bytes|filesizeformat }}</dd>
{% else %} </div>
{% for file in version.files.all %} {% endif %}
<dd>{{ file.size_bytes|filesizeformat }} ({{ file.get_platforms|join:", " }})</dd>
{% endfor %}
{% endif %}
</div>
</div> </div>
<div class="dl-row"> <div class="dl-row">
<div class="dl-col"> <div class="dl-col">
@ -115,9 +111,14 @@
<div class="btn-col"> <div class="btn-col">
{% with download_list=version.get_download_list %} {% with download_list=version.get_download_list %}
{% for download_item in download_list %} {% for download_item in download_list %}
<a href="{{ download_item.url }}" download="{{ download_item.name }}" class="btn btn-primary btn-block"> <a href="{{ download_item.url }}" download="{{ download_item.name }}" class="btn btn-primary btn-block d-flex justify-content-between text-start">
<i class="i-download"></i> <span>
<span>{% trans 'Download' %} v{{ version.version }} {{ download_item.platform }}</span> {% with platform=download_item.platform %}
<i class="i-{{ platform.name_first_word|lower }} me-1"></i> {{ platform.name }}
{% endwith %}
v{{ version.version }}
</span>
<span>{{ download_item.size|filesizeformat }}</span>
</a> </a>
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}