diff --git a/common/static/common/scripts/app.js b/common/static/common/scripts/app.js index 67d8865c..c281ef64 100644 --- a/common/static/common/scripts/app.js +++ b/common/static/common/scripts/app.js @@ -100,13 +100,120 @@ const btnInstallAction = document.querySelector('.js-btn-install-action'); 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 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) { 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 btnInstall.addEventListener('click', function() { // Hide btnInstallGroup diff --git a/common/static/common/styles/_extension.sass b/common/static/common/styles/_extension.sass index 07826cca..80757025 100644 --- a/common/static/common/styles/_extension.sass +++ b/common/static/common/styles/_extension.sass @@ -167,8 +167,9 @@ .btn-install-action-item +margin(3, bottom) - &:last-child - +margin(0, bottom) + .btn-install-action-item-active ~ .btn-install-action-item-active + border-top: thin solid var(--border-color) + +padding(3, top) .btn-install-drag, .btn-install-drag:active @@ -474,3 +475,11 @@ a .featured-image-preview 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) diff --git a/common/static/common/styles/_utilities.sass b/common/static/common/styles/_utilities.sass index 83ffe1a0..3972e792 100644 --- a/common/static/common/styles/_utilities.sass +++ b/common/static/common/styles/_utilities.sass @@ -35,6 +35,9 @@ .mw-#{$i} min-width: var(--spacer-#{$i}) +.list-style-none + list-style: none + .opacity-50 opacity: 0.5 diff --git a/extensions/migrations/0042_platform_name.py b/extensions/migrations/0042_platform_name.py new file mode 100644 index 00000000..68dec4c2 --- /dev/null +++ b/extensions/migrations/0042_platform_name.py @@ -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), + ] diff --git a/extensions/models.py b/extensions/models.py index cea2742f..9030a530 100644 --- a/extensions/models.py +++ b/extensions/models.py @@ -64,6 +64,20 @@ class Platform(CreatedModifiedMixin, models.Model): def get_by_slug(cls, slug: str): 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): @property @@ -706,6 +720,18 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model): return file 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 def is_listed(self): # 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), } ] - platform2file = {} + platform_slug2file = {} for file in files: platforms = file.get_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'is not platform-specific' ) - for platform in platforms: - platform2file[platform] = file + for platform_slug in platforms: + platform_slug2file[platform_slug] = file + all_platforms_by_slug = {p.slug: p for p in Platform.objects.all()} return [ { 'name': self.get_download_name(file), - 'platform': p, + 'platform': all_platforms_by_slug.get(platform_slug), 'size': file.size_bytes, '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]: diff --git a/extensions/templates/extensions/base.html b/extensions/templates/extensions/base.html index 5503d136..f502d0a9 100644 --- a/extensions/templates/extensions/base.html +++ b/extensions/templates/extensions/base.html @@ -6,7 +6,7 @@ {% has_maintainer extension as is_maintainer %} {% with latest=extension.latest_version %} -