added legacy flavor + image caching

This commit is contained in:
nvrl committed 2026-02-16 13:08:47 +01:00
1 parent fb266fab49
commit 33ae19e349
155 files changed
+407 -248

No files matched your search

@@ -1,144 +1,95 @@
import json
import os
import urllib.request
from django.core.management.base import BaseCommand
from django.core.files import File
from tempfile import NamedTemporaryFile
from django.conf import settings
from api.models import Category, Flavor
class Command(BaseCommand):
help = 'Adds legacy flavors that are no longer in the official API or are old versions'
help = 'Adds legacy flavors from JSON files in the legacy/ folder'
def download_image(self, url, flavor_name):
if not url:
return None
try:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response:
img_temp = NamedTemporaryFile(delete=True)
img_temp.write(response.read())
img_temp.flush()
ext = url.split('.')[-1].split('?')[0]
if len(ext) > 4: ext = 'png'
filename = f"legacy_{flavor_name.replace(' ', '_').lower()}.{ext}"
return File(img_temp, name=filename)
except Exception as e:
self.stdout.write(self.style.WARNING(f"Could not download image for {flavor_name}: {e}"))
return None
def handle(self, *args, **options):
# Ensure categories exist
energy = Category.objects.get(slug='energy')
hydration = Category.objects.get(slug='hydration')
iced_tea = Category.objects.get(slug='iced-tea')
# 1. Remove old legacy flavors (ones without external_id or marked as legacy)
# To be safe, we only remove those marked as legacy or with no external_id
removed_count, _ = Flavor.objects.filter(is_legacy=True).delete()
Flavor.objects.filter(external_id__isnull=True).delete()
self.stdout.write(self.style.SUCCESS(f"Removed {removed_count} old legacy flavors."))
legacy_data = [
{
"Name": "Hitschies Saure Drachenzunge (Legacy)",
"Geschmack": "Blaue Himbeere",
"Beschreibung": "Eine legendäre Kollaboration mit Hitschies. Schmeckt wie die blauen sauren Drachenzungen – süß, fruchtig und sauer.",
"Status": "Limited Edition / Vaulted",
"Bild_URL": "https://weareholy.com/cdn/shop/files/Holy_Energy_Hitschies_Sour_Dragon_Tongue_Tub_720x.png"
},
{
"Name": "Hitschies Saure Drachenzunge Rot (Legacy)",
"Geschmack": "Erdbeere & Kirsche",
"Beschreibung": "Die rote Variante der Hitschies-Kollaboration. Fruchtiger Mix aus roten Beeren, angelehnt an die roten Drachenzungen.",
"Status": "Limited Edition",
"Bild_URL": "https://weareholy.com/cdn/shop/files/Holy_Energy_Hitschies_Sour_Dragon_Tongue_Red_Tub_720x.png"
},
{
"Name": "Lion's Lemonade (Legacy)",
"Geschmack": "Mango & Kiwi",
"Beschreibung": "Einer der beliebtesten Klassiker. Ein tropischer, fruchtiger Mix für maximalen Fokus. Oft von Fans zurückgefordert.",
"Status": "Vaulted",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Lions_Lemonade_Tub_720x.png"
},
{
"Name": "Lollipop Lovebird (Legacy)",
"Geschmack": "Kirsch-Lolli",
"Beschreibung": "Erinnert an die klassischen roten Kirsch-Lollies aus der Kindheit. Sehr süß und intensiv.",
"Status": "Limited Edition",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Lollipop_Lovebird_Tub_720x.png"
},
{
"Name": "Frosty Fox (Legacy)",
"Geschmack": "Eisbonbon",
"Beschreibung": "Ein kühler, süßer Geschmack, der an die klassischen Eisbonbons erinnert. Erfrischend mit einer 'kühlen' Note.",
"Status": "Limited Edition",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Frosty_Fox_Tub_720x.png"
},
{
"Name": "Bubble Gum Butterfly (Legacy)",
"Geschmack": "Kaugummi",
"Beschreibung": "Der typische rosa Kaugummi-Geschmack (Bubblegum). Sehr süß und nostalgisch.",
"Status": "Limited Edition",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Bubblegum_Butterfly_Tub_720x.png"
},
{
"Name": "Woodruff Wolf (Legacy)",
"Geschmack": "Waldmeister",
"Beschreibung": "Ein traditioneller deutscher Geschmack. Süß-würzig, wie Wackelpudding oder Waldmeister-Sirup.",
"Status": "Limited Edition",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Woodruff_Wolf_Tub_720x.png"
},
{
"Name": "Baked Apple Boar (Legacy)",
"Geschmack": "Bratapfel",
"Beschreibung": "Eine winterliche Sonderedition mit warmen Noten von Apfel und Zimt.",
"Status": "Seasonal / Vaulted",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Baked_Apple_Boar_Tub_720x.png"
},
{
"Name": "Kölle Kamelle (Legacy)",
"Geschmack": "Fruchtbonbons (Karnevalsmischung)",
"Beschreibung": "Spezialedition zum Kölner Karneval. Schmeckt nach einem Mix aus verschiedenen Fruchtbonbons ('Kamelle').",
"Status": "Limited Edition",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Koelle_Kamelle_Tub_720x.png"
},
{
"Name": "Strawberry Shark (Legacy)",
"Geschmack": "Erdbeere & Mandarine",
"Beschreibung": "Ein fruchtiger Mix aus süßer Erdbeere und spritziger Mandarine. Teil des ursprünglichen 'Squads'.",
"Status": "Discontinued",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Strawberry_Shark_Tub_720x.png"
},
{
"Name": "Blueberry Bear (Legacy)",
"Geschmack": "Blaubeere & Kokosnuss",
"Beschreibung": "Eine weiche Beeren-Note mit einem exotischen Kokos-Abgang.",
"Status": "Discontinued",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Blueberry_Bear_Tub_720x.png"
},
{
"Name": "Thai Lime Toucan (Legacy)",
"Geschmack": "Kaffir-Limette",
"Beschreibung": "Sehr aromatisch und zesty. Unterscheidet sich von normaler Zitrone durch die herbe Kaffir-Note.",
"Status": "Discontinued",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Thai_Lime_Toucan_Tub_720x.png"
},
{
"Name": "Cactus Camel (Legacy)",
"Geschmack": "Kaktusfeige",
"Beschreibung": "Erfrischend, leicht melonenartig und nicht zu süß. Ein sehr eigener, beliebter Geschmack.",
"Status": "Discontinued",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Cactus_Camel_Tub_720x.png"
},
{
"Name": "Apple Alligator (Legacy)",
"Geschmack": "Grüner Apfel",
"Beschreibung": "Knackiger, saurer Apfelgeschmack. Sehr erfrischend.",
"Status": "Discontinued",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Apple_Alligator_Tub_720x.png"
},
{
"Name": "Cherry Cheetah (Legacy)",
"Geschmack": "Kirsche",
"Beschreibung": "Klassischer, fruchtiger Kirschgeschmack.",
"Status": "Discontinued",
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Cherry_Cheetah_Tub_720x.png"
}
]
# Ensure main categories exist
energy = Category.objects.get_or_create(name='Energy', defaults={'slug': 'energy'})[0]
hydration = Category.objects.get_or_create(name='Hydration', defaults={'slug': 'hydration'})[0]
iced_tea = Category.objects.get_or_create(name='Iced Tea', defaults={'slug': 'iced-tea'})[0]
packs = Category.objects.get_or_create(name='Packs and other', defaults={'slug': 'packs-and-other'})[0]
for data in legacy_data:
full_description = f"**Geschmack:** {data['Geschmack']}\n\n{data['Beschreibung']}\n\n**Status:** {data['Status']}"
legacy_dir = os.path.join(settings.BASE_DIR, '..', 'legacy')
if not os.path.exists(legacy_dir):
self.stdout.write(self.style.ERROR(f"Legacy directory not found at {legacy_dir}"))
return
cat = energy # Default
if "hydration" in data['Name'].lower() or "hydration" in data['Beschreibung'].lower():
cat = hydration
elif "iced tea" in data['Name'].lower() or "eistee" in data['Name'].lower():
cat = iced_tea
json_files = [f for f in os.listdir(legacy_dir) if f.endswith('.json')]
flavor, created = Flavor.objects.update_or_create(
name=data['Name'],
defaults={
'category': cat,
'description': full_description,
'image_url': data['Bild_URL'],
'is_available': False,
'external_id': None
}
)
created_count = 0
if created:
self.stdout.write(self.style.SUCCESS(f"Added legacy flavor: {data['Name']}"))
else:
self.stdout.write(self.style.SUCCESS(f"Updated legacy flavor: {data['Name']}"))
for json_file in json_files:
file_path = os.path.join(legacy_dir, json_file)
with open(file_path, 'r', encoding='utf-8') as f:
data_list = json.load(f)
for data in data_list:
name = data.get('Name')
desc = data.get('Beschreibung')
taste = data.get('Geschmack')
status_text = data.get('Status')
image_url = data.get('Bild_URL')
full_desc = f"**Geschmack:** {taste}\n\n{desc}\n\n**Status:** {status_text}"
# Determine category
cat = energy
if "hydration" in name.lower() or "hydration" in desc.lower():
cat = hydration
elif "iced tea" in name.lower() or "eistee" in name.lower():
cat = iced_tea
elif any(k in name.lower() for k in ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'shaker', 'starter']):
cat = packs
flavor = Flavor(
name=name,
category=cat,
description=full_desc,
image_url=image_url,
is_available=False,
is_legacy=True,
external_id=None
)
if image_url:
img_file = self.download_image(image_url, name)
if img_file:
flavor.image.save(img_file.name, img_file, save=False)
flavor.save()
created_count += 1
self.stdout.write(f"Added legacy flavor: {name}")
self.stdout.write(self.style.SUCCESS(f"Finished! Created {created_count} legacy flavors from JSON."))
+45 -16
View File
@@ -1,8 +1,10 @@
import json
import urllib.request
import re
import os
from django.core.management.base import BaseCommand
from django.utils.text import slugify
from django.core.files import File
from tempfile import NamedTemporaryFile
from api.models import Category, Flavor
class Command(BaseCommand):
@@ -15,12 +17,34 @@ class Command(BaseCommand):
cleantext = re.sub(cleanr, '', raw_html)
return cleantext.strip()
def download_image(self, url, flavor_name):
if not url:
return None
try:
# Simple caching: check if we already have a local image for this URL
# (In a real app, you might compare filenames or hashes)
img_temp = NamedTemporaryFile(delete=True)
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response:
img_temp.write(response.read())
img_temp.flush()
# Use extension from URL or default to .png
ext = url.split('.')[-1].split('?')[0]
if len(ext) > 4: ext = 'png'
filename = f"{flavor_name.replace(' ', '_').lower()}.{ext}"
return File(img_temp, name=filename)
except Exception as e:
self.stdout.write(self.style.WARNING(f"Could not download image for {flavor_name}: {e}"))
return None
def handle(self, *args, **options):
url = "https://weareholy.com/products.json?limit=250"
self.stdout.write(f"Fetching from {url}...")
try:
with urllib.request.urlopen(url) as response:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
except Exception as e:
self.stdout.write(self.style.ERROR(f"Failed to fetch data: {e}"))
@@ -54,7 +78,6 @@ class Command(BaseCommand):
tags_lower = [t.lower() for t in tags]
title_lower = title.lower()
# Check for Packs/Bundles/Samples
is_pack = False
pack_keywords = ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'starter', 'collection', 'probier']
if any(k in title_lower for k in pack_keywords):
@@ -62,7 +85,6 @@ class Command(BaseCommand):
if 'shaker' in title_lower or 'merch' in tags_lower:
is_pack = True
# Determine Category
if is_pack:
category = cat_map['packs']
elif 'holy energy' in tags_lower or 'energy' in tags_lower:
@@ -80,52 +102,59 @@ class Command(BaseCommand):
else:
continue
# Check availability
variants = p.get('variants', [])
is_available = any(v.get('available') for v in variants)
# Image
images = p.get('images', [])
image_url = images[0].get('src') if images else None
# Description
description = self.clean_html(p.get('body_html', ''))
# Shop URL
handle = p.get('handle')
shop_url = f"https://weareholy.com/products/{handle}" if handle else None
# Create or update
flavor = Flavor.objects.filter(external_id=p['id']).first()
if not flavor:
flavor = Flavor.objects.filter(name=title).first()
flavor = Flavor.objects.filter(name=title, is_legacy=False).first()
if flavor:
flavor.external_id = p['id']
flavor.name = title
flavor.category = category
flavor.description = description
flavor.image_url = image_url
flavor.shop_url = shop_url
flavor.is_available = is_available
flavor.is_legacy = False # API flavors are not legacy
# Cache image if URL changed or local image missing
if image_url and (flavor.image_url != image_url or not flavor.image):
img_file = self.download_image(image_url, title)
if img_file:
flavor.image.save(img_file.name, img_file, save=False)
flavor.image_url = image_url
flavor.save()
updated_count += 1
else:
Flavor.objects.create(
new_flavor = Flavor(
external_id=p['id'],
name=title,
category=category,
description=description,
image_url=image_url,
shop_url=shop_url,
is_available=is_available
is_available=is_available,
is_legacy=False
)
if image_url:
img_file = self.download_image(image_url, title)
if img_file:
new_flavor.image.save(img_file.name, img_file, save=False)
new_flavor.save()
created_count += 1
synced_external_ids.append(p['id'])
# Mark flavors NOT in the sync as unavailable (only for those that have an external_id)
# This keeps manually added flavors (with external_id=None) as they are.
# These are discontinued API flavors.
discontinued_count = Flavor.objects.filter(external_id__isnull=False).exclude(external_id__in=synced_external_ids).update(is_available=False)
self.stdout.write(self.style.SUCCESS(f"Finished! Created: {created_count}, Updated: {updated_count}, Marked Unavailable: {discontinued_count}"))
@@ -0,0 +1,28 @@
# Generated by Django 6.0.2 on 2026-02-16 12:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0006_reply'),
]
operations = [
migrations.AddField(
model_name='flavor',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='flavors/'),
),
migrations.AddField(
model_name='flavor',
name='is_legacy',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='flavor',
name='image_url',
field=models.URLField(blank=True, max_length=1000, null=True),
),
]
+9 -1
View File
@@ -31,9 +31,11 @@ class Flavor(models.Model):
category = models.ForeignKey(Category, related_name='flavors', on_delete=models.CASCADE)
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True)
image_url = models.URLField(max_length=500, blank=True, null=True)
image_url = models.URLField(max_length=1000, blank=True, null=True)
image = models.ImageField(upload_to='flavors/', blank=True, null=True)
shop_url = models.URLField(max_length=500, blank=True, null=True)
is_available = models.BooleanField(default=True)
is_legacy = models.BooleanField(default=False)
external_id = models.BigIntegerField(unique=True, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
@@ -47,6 +49,12 @@ class Flavor(models.Model):
return sum(r.score for r in ratings) / ratings.count()
return 0.0
@property
def cached_image_url(self):
if self.image:
return self.image.url
return self.image_url
class Rating(models.Model):
user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE)
flavor = models.ForeignKey(Flavor, related_name='ratings', on_delete=models.CASCADE)
+19 -2
View File
@@ -22,23 +22,40 @@ class ReplySerializer(serializers.ModelSerializer):
class RatingSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(read_only=True)
flavor_name = serializers.CharField(source='flavor.name', read_only=True)
flavor_image = serializers.CharField(source='flavor.image_url', read_only=True)
flavor_image = serializers.SerializerMethodField()
replies = ReplySerializer(many=True, read_only=True)
class Meta:
model = Rating
fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at', 'replies']
def get_flavor_image(self, obj):
if obj.flavor.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.flavor.image.url)
return obj.flavor.image.url
return obj.flavor.image_url
class FlavorSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
category_slug = serializers.CharField(source='category.slug', read_only=True)
average_rating = serializers.FloatField(read_only=True)
user_rating = serializers.SerializerMethodField()
ratings = RatingSerializer(many=True, read_only=True)
image_url = serializers.SerializerMethodField()
class Meta:
model = Flavor
fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'shop_url']
fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'is_legacy', 'shop_url']
def get_image_url(self, obj):
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return obj.image_url
def get_user_rating(self, obj):
request = self.context.get('request')
+3
View File
@@ -130,6 +130,9 @@ STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# --- Reverse Proxy Configuration ---
# These settings are crucial for running behind a proxy like Traefik
USE_X_FORWARDED_HOST = True
+3 -1
View File
@@ -2,6 +2,8 @@ from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
@@ -11,4 +13,4 @@ urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff. Show more