From a3c1b1d753e0ef2bfbef2f36d102cf82f0dc0816 Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 14:01:39 +0200
Subject: [PATCH 01/44] Fix: Settings Secondary Color in LIght THeme
---
public/styles/theme/light-theme.css | 2 +-
theme/template.theme.json | 82 +++++++++++++++++++++++++++++
2 files changed, 83 insertions(+), 1 deletion(-)
create mode 100644 theme/template.theme.json
diff --git a/public/styles/theme/light-theme.css b/public/styles/theme/light-theme.css
index 4b1d2b1..1ce4b7d 100644
--- a/public/styles/theme/light-theme.css
+++ b/public/styles/theme/light-theme.css
@@ -42,7 +42,7 @@ body.light {
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #cbcbcb;
- --settings-bg-secondary-color: #cbcbcb;
+ --settings-bg-secondary-color: #b8b8b8;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #656565;
}
diff --git a/theme/template.theme.json b/theme/template.theme.json
new file mode 100644
index 0000000..f867f2d
--- /dev/null
+++ b/theme/template.theme.json
@@ -0,0 +1,82 @@
+{
+ "meta": {
+ "name": "",
+ "author": "",
+ "version": "1.0.0",
+ "formatVersion": "1.0"
+ },
+
+ "style": {
+ "background": {
+ "image": "",
+ "size": "cover",
+ "position": "center",
+ "attachment": "fixed"
+ },
+
+ "colors": {
+ "background": "",
+ "text": {
+ "default": "",
+ "subtitle": "",
+ "mp3": ""
+ }
+ },
+
+ "form": {
+ "background": "",
+ "input": {
+ "background": "",
+ "border": "",
+ "borderFocus": "",
+ "text": "",
+ "placeholder": ""
+ },
+ "button": {
+ "background": "",
+ "text": "",
+ "hover": ""
+ }
+ },
+
+ "checkbox": {
+ "background": {
+ "default": "",
+ "checked": ""
+ },
+ "checkmarkBorder": "",
+ "pulse": ""
+ },
+
+ "download": {
+ "status": ""
+ },
+
+ "videoInfo": {
+ "background": "",
+ "text": "",
+ "heading": "",
+ "list": "",
+ "strong": "",
+ "link": "",
+ "linkHover": ""
+ },
+
+ "playlist": {
+ "background": ""
+ },
+
+ "settings": {
+ "button": {
+ "background": "",
+ "text": ""
+ },
+ "background": {
+ "primary": "",
+ "secondary": ""
+ },
+ "text": "",
+ "subtitle": ""
+ }
+ }
+}
\ No newline at end of file
From 24460cf0b5d9c2f9436717371e2f0833f21555a7 Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 14:20:05 +0200
Subject: [PATCH 02/44] Refactor: Restructure HTML layout and enhance CSS
styles for improved UI
---
public/index.html | 160 ++++++++++++++++++++-------------------
public/styles/styles.css | 94 ++++++++++++++---------
2 files changed, 141 insertions(+), 113 deletions(-)
diff --git a/public/index.html b/public/index.html
index 40970a4..4814b89 100644
--- a/public/index.html
+++ b/public/index.html
@@ -17,20 +17,22 @@
-
-
-
+
diff --git a/public/styles/components/checkbox.css b/public/styles/components/checkbox.css
index bfb8fc4..693b2b5 100644
--- a/public/styles/components/checkbox.css
+++ b/public/styles/components/checkbox.css
@@ -15,13 +15,13 @@
}
.checkmark {
- --clr: var(--checkbox-bg-checked);
+ --clr: var(--checkbox-checked-bg-color);
position: relative;
top: 0;
left: 0;
height: 1.3em;
width: 1.3em;
- background-color: var(--checkbox-bg-default);
+ background-color: var(--checkbox-unchecked-bg-color);
border-radius: .5rem;
transition: background-color 300ms, border-radius 300ms;
}
diff --git a/public/styles/components/loader.css b/public/styles/components/loader.css
index 812b5a3..eca3caa 100644
--- a/public/styles/components/loader.css
+++ b/public/styles/components/loader.css
@@ -4,9 +4,9 @@
gap: 10px;
padding: 10px 20px;
background: var(--form-bg-color);
- border: 2px solid var(--mp3-text-color);
+ border: 2px solid var(--audio-only-label-color);
border-radius: 8px;
- color: var(--mp3-text-color);
+ color: var(--audio-only-label-color);
font-weight: bold;
animation: pulse 1.2s infinite;
}
diff --git a/public/styles/components/progressBar.css b/public/styles/components/progressBar.css
index 7a38726..70a9630 100644
--- a/public/styles/components/progressBar.css
+++ b/public/styles/components/progressBar.css
@@ -1,22 +1,34 @@
+/* Container pour isoler la progress bar */
+.progress-container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 0;
+ margin-top: 20px;
+}
+
/* Wrapper de la barre de téléchargement */
#downloadProgressWrapper {
width: 100%;
max-width: 400px;
height: 12px;
- background-color: #444;
- border-radius: 6px;
+ background-color: var(--progress-bar-bg-color, #cccccc);
+ border-radius: 10px;
overflow: hidden;
- margin: 10px auto;
+ margin: 0;
display: none;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
+ box-sizing: border-box;
}
/* Barre de progression */
#downloadProgress {
height: 100%;
width: 0%;
- background-color: var(--video-info-link-color, #4caf50);
- border-radius: 6px 0 0 6px;
+ background-color: var(--progress-bar-fill-color, #007bff);
+ border-radius: 10px;
transition: width 0.2s ease;
}
diff --git a/public/styles/layout/form.css b/public/styles/layout/form.css
index 3a540bc..4ef5349 100644
--- a/public/styles/layout/form.css
+++ b/public/styles/layout/form.css
@@ -50,7 +50,7 @@ form#downloadForm {
align-items: center;
align-self: center;
gap: 0.5em;
- color: var(--form-input-text-color);
+ color: var(--audio-only-label-color);
}
#UrlInput {
@@ -108,6 +108,7 @@ form#downloadForm {
}
.paste-btn svg {
+ color: var(--paste-button-icon-color);
width: 30px;
height: 30px;
}
diff --git a/public/styles/layout/header.css b/public/styles/layout/header.css
index f2ba12f..76f7f62 100644
--- a/public/styles/layout/header.css
+++ b/public/styles/layout/header.css
@@ -1,11 +1,13 @@
header {
+ position: fixed;
+ top: 0;
+ width: 100%;
+ height: 48px;
+ z-index: 1100;
display: flex;
- flex-direction: column;
- align-items: center;
- margin-bottom: 2vw;
+ align-items: center;
}
-
-header h1{
- margin-bottom: 0.2vw;
+#title {
+ color: var(--default-text-color);
}
\ No newline at end of file
diff --git a/public/styles/layout/settingsPanel.css b/public/styles/layout/settingsPanel.css
index 0fa7b3d..e7a2c98 100644
--- a/public/styles/layout/settingsPanel.css
+++ b/public/styles/layout/settingsPanel.css
@@ -40,7 +40,7 @@
width: 40vw;
max-width: 320px;
max-height: 80vh;
- background-color: var(--settings-bg-primary-color);
+ background-color: var(--settings-modal-bg-color);
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
@@ -89,7 +89,7 @@
flex-direction: column;
margin-bottom: 20px;
padding: 10px 15px;
- background: var(--settings-bg-secondary-color);
+ background: var(--settings-section-bg-color);
border-radius: 12px;
gap: 6px;
}
diff --git a/public/styles/layout/topbar.css b/public/styles/layout/topbar.css
index 939b931..158d8d2 100644
--- a/public/styles/layout/topbar.css
+++ b/public/styles/layout/topbar.css
@@ -2,20 +2,12 @@
display: flex;
align-items: center;
justify-content: space-between;
-
- position: fixed;
- top: 0;
- z-index: 1100;
-
- height: 48px;
width: 100%;
-
+ height: 100%;
background: #1f1f1f98;
color: #eaeaea;
-
-webkit-app-region: drag;
user-select: none;
-
}
/* LEFT - custom buttons */
diff --git a/public/styles/layout/videoinfo.css b/public/styles/layout/videoinfo.css
index 79ecb98..f897421 100644
--- a/public/styles/layout/videoinfo.css
+++ b/public/styles/layout/videoinfo.css
@@ -4,7 +4,7 @@
margin-left: auto;
margin-right: auto;
padding: 1.5em 2em;
- background-color: var(--infos-box-color);
+ background-color: var(--video-info-bg-color);
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
color: var(--video-info-text-color);
diff --git a/public/styles/styles.css b/public/styles/styles.css
index 665485d..fad369e 100644
--- a/public/styles/styles.css
+++ b/public/styles/styles.css
@@ -1,18 +1,20 @@
/* Layout */
-@import url("layout/form.css");
-@import url("layout/settingsPanel.css");
-@import url("layout/container.css");
+@import url("layout/topbar.css");
@import url("layout/header.css");
+@import url("layout/form.css");
+@import url("layout/container.css");
+@import url("layout/settingsPanel.css");
@import url("layout/videoinfo.css");
/* Components */
@import url("components/checkbox.css");
@import url("components/loader.css");
-
-@import url("theme/themeimport.css");
@import url("components/editpathbutton.css");
@import url("components/progressBar.css");
+/* Themes */
+@import url("theme/themeimport.css");
+
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
#app {
@@ -45,21 +47,9 @@ html,body{
font-style: normal;
}
-html {
- height: 100%;
-}
-
-body {
- margin: 0;
- padding: 0;
- background-color: var(--background-color);
- width: 100%;
- height: 100%;
- color: var(--default-text-color);
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
+body{
+ background-color: var(--background-color);
+ color: var(--default-text-color);
}
header {
@@ -81,6 +71,7 @@ main {
justify-content: flex-start;
align-items: center;
padding: 0;
+ gap: 0;
}
/* Scrollbar pour main */
@@ -128,10 +119,6 @@ body.light main::-webkit-scrollbar-thumb:hover {
color: var(--subtitle-color);
}
-.checkbox-inline{
- color: var(--mp3-text-color);
-}
-
body,
.container,
input,
diff --git a/public/styles/theme/chirac-theme.css b/public/styles/theme/chirac-theme.css
index 2645be5..babeebd 100644
--- a/public/styles/theme/chirac-theme.css
+++ b/public/styles/theme/chirac-theme.css
@@ -9,7 +9,7 @@ body.chirac {
--background-color: rgba(255, 255, 255, 0.85);
--default-text-color: #002395;
--subtitle-color: #EF4135;
- --mp3-text-color: #002395;
+ --audio-only-label-color: #002395;
/* Formulaire */
--form-bg-color: rgba(255, 255, 255, 0.9);
@@ -21,18 +21,23 @@ body.chirac {
--form-button-bg-color: #002395;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #EF4135;
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #cccccc;
- --checkbox-bg-checked: #002395;
+ --checkbox-unchecked-bg-color: #cccccc;
+ --checkbox-checked-bg-color: #002395;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(0, 35, 149, 0.5);
/* Download status */
--download-status-color: #002395;
+ /* Progress bar */
+ --progress-bar-bg-color: #d0d0d0;
+ --progress-bar-fill-color: #EF4135;
+
/* Video Info Box */
- --infos-box-color: rgba(255, 255, 255, 0.9);
+ --video-info-bg-color: rgba(255, 255, 255, 0.9);
--video-info-text-color: #000000;
--video-info-heading-color: #EF4135;
--video-info-list-color: #444444;
@@ -47,8 +52,8 @@ body.chirac {
--settings-button-bg-color: #00196a;
--settings-button-text-color: #dcdcdc;
- --settings-bg-primary-color: #0b0528;
- --settings-bg-secondary-color: #1c1055;
+ --settings-modal-bg-color: #0b0528;
+ --settings-section-bg-color: #1c1055;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
diff --git a/public/styles/theme/dark-theme.css b/public/styles/theme/dark-theme.css
index e1bd48f..a727ac0 100644
--- a/public/styles/theme/dark-theme.css
+++ b/public/styles/theme/dark-theme.css
@@ -3,7 +3,7 @@ body.dark {
--background-color: #121212; /* noir/gris très foncé */
--default-text-color: #e0e0e0; /* texte clair */
--subtitle-color: #2196f3; /* bleu accent */
- --mp3-text-color: #f5f5f5; /* texte checkbox */
+ --audio-only-label-color: #f5f5f5; /* texte checkbox */
/* Formulaire */
--form-bg-color: #1e1e1e; /* fond formulaire */
@@ -15,18 +15,23 @@ body.dark {
--form-button-bg-color: #2196f3; /* bouton bleu */
--form-button-text-color: #ffffff; /* texte bouton */
--form-button-bg-hover-color: #1976d2; /* hover bleu plus foncé */
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #555555; /* gris moyen */
- --checkbox-bg-checked: #2196f3; /* bleu accent */
+ --checkbox-unchecked-bg-color: #555555; /* gris moyen */
+ --checkbox-checked-bg-color: #2196f3; /* bleu accent */
--checkbox-checkmark-border-color: #ffffff; /* blanc */
--checkbox-pulse-color: rgba(33, 150, 243, 0.5);
/* Download status */
--download-status-color: #2196f3;
+ /* Progress bar */
+ --progress-bar-bg-color: #444444;
+ --progress-bar-fill-color: #2196f3;
+
/* Video Info Box */
- --infos-box-color: #1a1a1a;
+ --video-info-bg-color: #1a1a1a;
--video-info-text-color: #e0e0e0;
--video-info-heading-color: #2196f3;
--video-info-list-color: #cccccc;
@@ -41,8 +46,8 @@ body.dark {
--settings-button-bg-color: #1e1e1e;
--settings-button-text-color: var(--default-text-color);
- --settings-bg-primary-color: #1e1e1e;
- --settings-bg-secondary-color: #2a2a2a;
+ --settings-modal-bg-color: #1e1e1e;
+ --settings-section-bg-color: #2a2a2a;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #aaa;
diff --git a/public/styles/theme/drift-theme.css b/public/styles/theme/drift-theme.css
index f55f365..eb640ab 100644
--- a/public/styles/theme/drift-theme.css
+++ b/public/styles/theme/drift-theme.css
@@ -9,7 +9,7 @@ body.drift {
--background-color: #0f1218;
--default-text-color: #3d4b61;
--subtitle-color: #1a1a1a;
- --mp3-text-color: #c0d4ff;
+ --audio-only-label-color: #c0d4ff;
/* Formulaire */
--form-bg-color: #1e222a;
@@ -21,18 +21,23 @@ body.drift {
--form-button-bg-color: #2a2979;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #353393;
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #2a2e38;
- --checkbox-bg-checked: #2a2979;
+ --checkbox-unchecked-bg-color: #2a2e38;
+ --checkbox-checked-bg-color: #2a2979;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(76,108,255,0.3);
/* Download status */
--download-status-color: #ff8e8e;
+ /* Progress bar */
+ --progress-bar-bg-color: #3b4b6d;
+ --progress-bar-fill-color: #ff8e8e;
+
/* Video Info Box */
- --infos-box-color: #1e222ad0;
+ --video-info-bg-color: #1e222ad0;
--video-info-text-color: #e8e8f0;
--video-info-heading-color: #9ab6ff;
--video-info-list-color: #c0d4ff;
@@ -47,8 +52,8 @@ body.drift {
--settings-button-bg-color: #1e222ace;
--settings-button-text-color: #b3b3b4;
- --settings-bg-primary-color: #1e222a;
- --settings-bg-secondary-color: #272d38;
+ --settings-modal-bg-color: #1e222a;
+ --settings-section-bg-color: #272d38;
--settings-text-color: #bababa;
--settings-subtitle-color: #cacaca;
diff --git a/public/styles/theme/fanatic-theme.css b/public/styles/theme/fanatic-theme.css
index 401fd67..a6d0d72 100644
--- a/public/styles/theme/fanatic-theme.css
+++ b/public/styles/theme/fanatic-theme.css
@@ -3,7 +3,7 @@ body.fanatic{
--background-color: #121212;
--default-text-color: #eee;
--subtitle-color: #FF5900;
- --mp3-text-color: #ffffff;
+ --audio-only-label-color: #ffffff;
/* Formulaire */
--form-bg-color: #1e1e1e;
@@ -15,18 +15,23 @@ body.fanatic{
--form-button-bg-color: #FF5900;
--form-button-text-color: #121212;
--form-button-bg-hover-color: #e65500;
+ --paste-button-icon-color: #f1f1f1;
/* Checkbox */
- --checkbox-bg-default: #555555;
- --checkbox-bg-checked: #ff6600;
+ --checkbox-unchecked-bg-color: #555555;
+ --checkbox-checked-bg-color: #ff6600;
--checkbox-checkmark-border-color: #eee;
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
/* Download status */
--download-status-color: #FF5900;
+ /* Progress bar */
+ --progress-bar-bg-color: #444444;
+ --progress-bar-fill-color: #FF5900;
+
/* Video Info Box */
- --infos-box-color: #222222;
+ --video-info-bg-color: #222222;
--video-info-text-color: #eee;
--video-info-heading-color: #FF5900;
--video-info-list-color: #ccc;
@@ -41,8 +46,8 @@ body.fanatic{
--settings-button-bg-color: #1e1e1e;
--settings-button-text-color: #dcdcdc;
- --settings-bg-primary-color: #1e1e1e;
- --settings-bg-secondary-color: #2d2d2d;
+ --settings-modal-bg-color: #1e1e1e;
+ --settings-section-bg-color: #2d2d2d;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
diff --git a/public/styles/theme/light-theme.css b/public/styles/theme/light-theme.css
index 1ce4b7d..aff1015 100644
--- a/public/styles/theme/light-theme.css
+++ b/public/styles/theme/light-theme.css
@@ -3,7 +3,7 @@ body.light {
--background-color: #dadada;
--default-text-color: #222222;
--subtitle-color: #007bff;
- --mp3-text-color: #000000;
+ --audio-only-label-color: #000000;
/* Formulaire */
--form-bg-color: #ffffff;
@@ -15,18 +15,23 @@ body.light {
--form-button-bg-color: #007bff;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #0056b3;
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #cccccc;
- --checkbox-bg-checked: #007bff;
+ --checkbox-unchecked-bg-color: #cccccc;
+ --checkbox-checked-bg-color: #007bff;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
/* Download status */
--download-status-color: #007bff;
+ /* Progress bar */
+ --progress-bar-bg-color: #e0e0e0;
+ --progress-bar-fill-color: #007bff;
+
/* Video Info Box */
- --infos-box-color: #ffffff;
+ --video-info-bg-color: #ffffff;
--video-info-text-color: #222222;
--video-info-heading-color: #007bff;
--video-info-list-color: #444444;
@@ -41,8 +46,8 @@ body.light {
--settings-button-bg-color: #e4e4e4;
--settings-button-text-color: var(--default-text-color);
- --settings-bg-primary-color: #cbcbcb;
- --settings-bg-secondary-color: #b8b8b8;
+ --settings-modal-bg-color: #cbcbcb;
+ --settings-section-bg-color: #b8b8b8;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #656565;
}
diff --git a/public/styles/theme/neon-theme.css b/public/styles/theme/neon-theme.css
index 63c87da..3097cd6 100644
--- a/public/styles/theme/neon-theme.css
+++ b/public/styles/theme/neon-theme.css
@@ -10,7 +10,7 @@ body.neon {
--background-color: #1b1b2f;
--default-text-color: #e0e0ff;
--subtitle-color: #a3a3ff;
- --mp3-text-color: #c0c0ff;
+ --audio-only-label-color: #c0c0ff;
/* Formulaire */
--form-bg-color: #2c2c4a;
@@ -22,18 +22,23 @@ body.neon {
--form-button-bg-color: #5858f0;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #7a7aff;
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #302b63;
- --checkbox-bg-checked: #5858f0;
+ --checkbox-unchecked-bg-color: #302b63;
+ --checkbox-checked-bg-color: #5858f0;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(88,88,240,0.3);
/* Download status */
--download-status-color: #a3a3ff;
+ /* Progress bar */
+ --progress-bar-bg-color: #302b63;
+ --progress-bar-fill-color: #a3a3ff;
+
/* Video Info Box */
- --infos-box-color: #2c2c4a;
+ --video-info-bg-color: #2c2c4a;
--video-info-text-color: #e0e0ff;
--video-info-heading-color: #a3a3ff;
--video-info-list-color: #c0c0ff;
@@ -48,8 +53,8 @@ body.neon {
--settings-button-bg-color: #202038;
--settings-button-text-color: var(--default-text-color);
- --settings-bg-primary-color: #242454;
- --settings-bg-secondary-color: #2f2f6f;
+ --settings-modal-bg-color: #242454;
+ --settings-section-bg-color: #2f2f6f;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #d2d2d2;
}
diff --git a/public/styles/theme/nf-theme.css b/public/styles/theme/nf-theme.css
index 5b7ac42..e61eec2 100644
--- a/public/styles/theme/nf-theme.css
+++ b/public/styles/theme/nf-theme.css
@@ -9,7 +9,7 @@ body.nf {
--background-color: #1a1a1a;
--default-text-color: #e6dfd5;
--subtitle-color: #f5f0e6;
- --mp3-text-color: #d8cfc2;
+ --audio-only-label-color: #d8cfc2;
/* Formulaire */
--form-bg-color: #2a2a2a;
@@ -21,18 +21,23 @@ body.nf {
--form-button-bg-color: #cbb89d;
--form-button-text-color: #1a1a1a;
--form-button-bg-hover-color: #b7a588;
+ --paste-button-icon-color: #1a1a1a;
/* Checkbox */
- --checkbox-bg-default: #4a4036;
- --checkbox-bg-checked: #cbb89d;
+ --checkbox-unchecked-bg-color: #4a4036;
+ --checkbox-checked-bg-color: #cbb89d;
--checkbox-checkmark-border-color: #1a1a1a;
--checkbox-pulse-color: rgba(203,184,157,0.3);
/* Download status */
--download-status-color: #cbb89d;
+ /* Progress bar */
+ --progress-bar-bg-color: #4a4036;
+ --progress-bar-fill-color: #cbb89d;
+
/* Video Info Box */
- --infos-box-color: #2a2a2a;
+ --video-info-bg-color: #2a2a2a;
--video-info-text-color: #e6dfd5;
--video-info-heading-color: #f5f0e6;
--video-info-list-color: #d8cfc2;
@@ -47,8 +52,8 @@ body.nf {
--settings-button-bg-color: #2a2a2a;
--settings-button-text-color: var(--default-text-color);
- --settings-bg-primary-color: #2a2a2a;
- --settings-bg-secondary-color: #313131;
+ --settings-modal-bg-color: #2a2a2a;
+ --settings-section-bg-color: #313131;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #7a7979;
}
diff --git a/public/styles/theme/songbird-theme.css b/public/styles/theme/songbird-theme.css
index 976d0cc..1280393 100644
--- a/public/styles/theme/songbird-theme.css
+++ b/public/styles/theme/songbird-theme.css
@@ -9,7 +9,7 @@ body.songbird {
--background-color: #0a0a0a; /* noir profond */
--default-text-color: #ff2a2a; /* rouge néon */
--subtitle-color: #f2f2f2; /* rouge sombre */
- --mp3-text-color: #ff4d4d; /* rouge un peu plus clair */
+ --audio-only-label-color: #ff4d4d; /* rouge un peu plus clair */
/* Formulaire */
--form-bg-color: rgba(20,0,0,0.85); /* noir semi-transparent avec rouge */
@@ -21,18 +21,23 @@ body.songbird {
--form-button-bg-color: #b30000;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #ff2a2a;
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #630000;
- --checkbox-bg-checked: #ff2a2a;
+ --checkbox-unchecked-bg-color: #630000;
+ --checkbox-checked-bg-color: #ff2a2a;
--checkbox-checkmark-border-color: #ffffff;
--checkbox-pulse-color: rgba(255,42,42,0.3);
/* Download status */
--download-status-color: #ff2a2a;
+ /* Progress bar */
+ --progress-bar-bg-color: #630000;
+ --progress-bar-fill-color: #ff2a2a;
+
/* Video Info Box */
- --infos-box-color: rgba(30,0,0,0.9);
+ --video-info-bg-color: rgba(30,0,0,0.9);
--video-info-text-color: #ff4d4d;
--video-info-heading-color: #ff2a2a;
--video-info-list-color: #b30000;
@@ -47,8 +52,8 @@ body.songbird {
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
--settings-button-text-color: var(--default-text-color);
- --settings-bg-primary-color: #121212;
- --settings-bg-secondary-color: #1a1a1a;
+ --settings-modal-bg-color: #121212;
+ --settings-section-bg-color: #1a1a1a;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #7a7979;
}
diff --git a/public/styles/theme/spicy-theme.css b/public/styles/theme/spicy-theme.css
index a001407..d1f5a00 100644
--- a/public/styles/theme/spicy-theme.css
+++ b/public/styles/theme/spicy-theme.css
@@ -3,7 +3,7 @@ body.spicy {
--background-color: #121212; /* fond noir profond */
--default-text-color: #ffffff; /* texte clair */
--subtitle-color: #f87918; /* accent rouge piment */
- --mp3-text-color: #ffffff; /* texte checkbox inline */
+ --audio-only-label-color: #ffffff; /* texte checkbox inline */
/* Formulaire */
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
@@ -15,18 +15,23 @@ body.spicy {
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
--form-button-text-color: #ffffff; /* texte bouton blanc */
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #555555; /* gris moyen */
- --checkbox-bg-checked: #ff1a1a; /* rouge piment */
+ --checkbox-unchecked-bg-color: #555555; /* gris moyen */
+ --checkbox-checked-bg-color: #ff1a1a; /* rouge piment */
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
/* Download status */
--download-status-color: #ffffff; /* rouge vif */
+ /* Progress bar */
+ --progress-bar-bg-color: #444444;
+ --progress-bar-fill-color: #ff1a1a;
+
/* Video Info Box */
- --infos-box-color: #1a1a1a; /* fond infos gris foncé */
+ --video-info-bg-color: #1a1a1a; /* fond infos gris foncé */
--video-info-text-color: #f5f5f5; /* texte clair */
--video-info-heading-color: #ff1a1a; /* titres rouges */
--video-info-list-color: #cccccc; /* texte liste gris clair */
@@ -41,8 +46,8 @@ body.spicy {
--settings-button-bg-color: #1a1a1a;
--settings-button-text-color: #dcdcdc;
- --settings-bg-primary-color: #1a1a1a;
- --settings-bg-secondary-color: #292929;
+ --settings-modal-bg-color: #1a1a1a;
+ --settings-section-bg-color: #292929;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
diff --git a/public/styles/variables.css b/public/styles/variables.css
index 29362d3..3107bef 100644
--- a/public/styles/variables.css
+++ b/public/styles/variables.css
@@ -3,7 +3,7 @@
--background-color: #001224;
--default-text-color: #eee;
--subtitle-color: #007bff;
- --mp3-text-color: #000000;
+ --audio-only-label-color: #000000;
/* Formulaire */
--form-bg-color: #ffffff;
@@ -15,18 +15,23 @@
--form-button-bg-color: #007bff;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #0056b3;
+ --paste-button-icon-color: #ffffff;
/* Checkbox */
- --checkbox-bg-default: #cccccc;
- --checkbox-bg-checked: #3B82F6;
+ --checkbox-unchecked-bg-color: #cccccc;
+ --checkbox-checked-bg-color: #3B82F6;
--checkbox-checkmark-border-color: #E0E0E2;
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
/* Download status */
--download-status-color: #007bff;
+ /* Progress bar */
+ --progress-bar-bg-color: #cccccc;
+ --progress-bar-fill-color: #007bff;
+
/* Video Info Box */
- --infos-box-color: #f0f0f0;
+ --video-info-bg-color: #f0f0f0;
--video-info-text-color: #222222;
--video-info-heading-color: #007bff;
--video-info-list-color: #444444;
@@ -38,8 +43,8 @@
--settings-button-bg-color: var(--form-bg-color);
--settings-button-text-color: var(--default-text-color);
- --settings-bg-primary-color: var(--form-bg-color);
- --settings-bg-secondary-color: var(--form-bg-color);
+ --settings-modal-bg-color: var(--form-bg-color);
+ --settings-section-bg-color: var(--form-bg-color);
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #aaa;
From 5c4e6669fee58b7981794a434c5d9c42679a7d52 Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 15:18:11 +0200
Subject: [PATCH 04/44] Fix: Update theme JSON structure for audio and button
settings
---
theme/template.theme.json | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/theme/template.theme.json b/theme/template.theme.json
index f867f2d..328615f 100644
--- a/theme/template.theme.json
+++ b/theme/template.theme.json
@@ -19,7 +19,7 @@
"text": {
"default": "",
"subtitle": "",
- "mp3": ""
+ "audioOnly": ""
}
},
@@ -36,12 +36,13 @@
"background": "",
"text": "",
"hover": ""
- }
+ },
+ "pasteButtonIcon": ""
},
"checkbox": {
"background": {
- "default": "",
+ "unchecked": "",
"checked": ""
},
"checkmarkBorder": "",
@@ -52,6 +53,11 @@
"status": ""
},
+ "progressBar": {
+ "background": "",
+ "fill": ""
+ },
+
"videoInfo": {
"background": "",
"text": "",
@@ -72,8 +78,8 @@
"text": ""
},
"background": {
- "primary": "",
- "secondary": ""
+ "modal": "",
+ "section": ""
},
"text": "",
"subtitle": ""
From 798084e3bf28bd8d411eeaa1fb94f890d10fba09 Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 16:00:51 +0200
Subject: [PATCH 05/44] Fix: Remove unusued element
---
package.json | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 25d9ebc..e75a900 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,7 @@
],
"files": [
"**/*",
- "node_modules/**/*",
- "!logs/*.json"
+ "node_modules/**/*"
],
"directories": {
"buildResources": "build"
From 0c7103c5c93110d746b3eb58e173b3a4a1b0a6c8 Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 16:13:36 +0200
Subject: [PATCH 06/44] Update: Readme version and file structure
---
README.md | 13 +++++++++++--
package.json | 2 +-
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 25dc446..111a79a 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
### **A clean, open-source multimedia downloader for Windows**
-[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
+[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[](https://www.gnu.org/licenses/gpl-3.0)
[](https://masteracnolo.github.io/FreedomLoader/)

@@ -213,10 +213,18 @@ Freedom-Loader/
├── config/ # Configuration files
├── public/ # Frontend assets (HTML, CSS, JavaScript)
├── ressources/ # Internal resources (icons, binaries)
+├── app/ # Electron main process modules
+│ ├── windowManager.js
+│ ├── ipcHandlers.js
+│ ├── pathValidator.js
+│ ├── ytDlpUpdater.js
+│ ├── autoUpdater.js
+│ ├── discordRPC.js
+│ └── dependencyCheck.js
├── server/ # Express server code
│ ├── routes/ # API route handlers
│ ├── services/ # Business logic
-│ └── utils/ # Server utilities
+│ └── helpers/ # Server helpers
├── .github/ # GitHub configuration and workflows
├── main.js # Electron main process
├── preload.js # Electron preload script
@@ -230,6 +238,7 @@ Freedom Loader uses a client-server architecture within a single Electron applic
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
- **Backend**: Express.js server running locally for download management
+- **App modules**: Electron-specific logic (window, IPC, updates, RPC) isolated in `app/`
- **IPC Bridge**: Secure communication via Electron's preload script
- **Logging**: Winston-based structured logging with file rotation
- **Updates**: Automatic checking and installation via electron-updater
diff --git a/package.json b/package.json
index e75a900..dfff853 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "freedom-loader",
"productName": "Freedom Loader",
- "version": "1.4.7",
+ "version": "1.5.0",
"author": "MasterAcnolo",
"description": "Freedom Loader",
"main": "main.js",
From 96401a6e1f77cd47dd97481d1eaf7dd86c32d89b Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 16:14:21 +0200
Subject: [PATCH 07/44] Refactor: split main.js into app/ modules
---
server/update.js => app/autoUpdater.js | 2 +-
app/dependencyCheck.js | 32 +++
{server => app}/discordRPC.js | 2 +-
app/ipcHandlers.js | 111 +++++++++
app/pathValidator.js | 32 +++
app/windowManager.js | 50 ++++
app/ytDlpUpdater.js | 12 +
main.js | 305 +++----------------------
server/server.js | 25 +-
9 files changed, 284 insertions(+), 287 deletions(-)
rename server/update.js => app/autoUpdater.js (96%)
create mode 100644 app/dependencyCheck.js
rename {server => app}/discordRPC.js (95%)
create mode 100644 app/ipcHandlers.js
create mode 100644 app/pathValidator.js
create mode 100644 app/windowManager.js
create mode 100644 app/ytDlpUpdater.js
diff --git a/server/update.js b/app/autoUpdater.js
similarity index 96%
rename from server/update.js
rename to app/autoUpdater.js
index d38aa70..0f4a4ac 100644
--- a/server/update.js
+++ b/app/autoUpdater.js
@@ -1,6 +1,6 @@
const { autoUpdater } = require("electron-updater");
const { app, Notification, shell} = require("electron");
-const { logger } = require("./logger");
+const { logger } = require("../server/logger");
function AutoUpdater() {
diff --git a/app/dependencyCheck.js b/app/dependencyCheck.js
new file mode 100644
index 0000000..ba285c9
--- /dev/null
+++ b/app/dependencyCheck.js
@@ -0,0 +1,32 @@
+const fs = require("fs");
+const { app, dialog } = require("electron");
+const { logger } = require("../server/logger");
+
+function checkNativeDependencies() {
+ const { binaryPaths } = require("../server/helpers/path.helpers");
+
+ const deps = [
+ { name: "yt-dlp.exe", path: binaryPaths.ytDlp },
+ { name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
+ { name: "ffprobe.exe", path: binaryPaths.ffprobe },
+ { name: "deno.exe", path: binaryPaths.deno },
+ ];
+
+ const missing = deps.filter(d => !fs.existsSync(d.path));
+ if (missing.length === 0) return true;
+
+ const list = missing.map(d => d.name).join(", ");
+ logger.error(`Missing dependencies: ${list}`);
+
+ app.whenReady().then(() => {
+ dialog.showErrorBox(
+ "Missing dependencies",
+ `The following files are missing in the 'ressources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
+ );
+ app.quit();
+ });
+
+ return false;
+}
+
+module.exports = { checkNativeDependencies };
\ No newline at end of file
diff --git a/server/discordRPC.js b/app/discordRPC.js
similarity index 95%
rename from server/discordRPC.js
rename to app/discordRPC.js
index 90c96d4..49b5416 100644
--- a/server/discordRPC.js
+++ b/app/discordRPC.js
@@ -1,6 +1,6 @@
const config = require('../config');
const RPC = require("discord-rpc");
-const { logger } = require("./logger");
+const { logger } = require("../server/logger");
const clientId = `${config.DiscordRPCID}`;
const rpc = new RPC.Client({ transport: "ipc" });
diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js
new file mode 100644
index 0000000..8e953a0
--- /dev/null
+++ b/app/ipcHandlers.js
@@ -0,0 +1,111 @@
+const { ipcMain, dialog, shell } = require("electron");
+const fs = require("fs");
+const path = require("path");
+const { logger, logDir } = require("../server/logger");
+const { configFeatures } = require("../config");
+const config = require("../config");
+const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
+
+const configFolderPath = path.join(
+ config.localMode
+
+ ? path.join(__dirname, "..")
+ : path.join(path.dirname(process.execPath), "resources"), "config", "config.json"
+);
+
+const FEATURE_WHITELIST = new Set([
+ "autoUpdate",
+ "discordRPC",
+ "customTopBar",
+ "autoCheckInfo",
+ "addThumbnail",
+ "addMetadata",
+ "verboseLogs",
+ "autoDownloadPlaylist",
+ "customCodec",
+]);
+
+function registerIpcHandlers(getMainWindow) {
+
+ // Infos générales
+ ipcMain.handle("version", () => config.version);
+ ipcMain.handle("features", () => configFeatures);
+
+ // Sélection et validation de dossier
+ ipcMain.handle("select-download-folder", async () => {
+ const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
+ if (result.canceled) {
+ logger.info("Folder selection cancelled by user");
+ return null;
+ }
+ try {
+ const validated = validateDownloadPath(result.filePaths[0]);
+ logger.info(`Folder selected and validated: ${validated}`);
+ return validated;
+ } catch (err) {
+ logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
+ throw err;
+ }
+ });
+
+ ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath));
+ ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath());
+
+ // Progression dans la taskbar
+ ipcMain.on("set-progress", (_, percent) => {
+ getMainWindow()?.setProgressBar(percent / 100);
+ });
+
+ // Contrôles fenêtre (custom top bar)
+ ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
+ ipcMain.on("window-maximize", () => {
+ const win = getMainWindow();
+ if (!win) return;
+ win.isMaximized() ? win.unmaximize() : win.maximize();
+ });
+ ipcMain.on("window-close", () => getMainWindow()?.close());
+
+ // Actions custom
+ ipcMain.on("open-devtools", () =>
+ getMainWindow()?.webContents.openDevTools({ mode: "detach" })
+ );
+ ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
+ ipcMain.on("open-website", () =>
+ shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html")
+ );
+ ipcMain.on("open-wiki", () =>
+ shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html")
+ );
+ ipcMain.on("open-config", () => shell.openPath(configFolderPath));
+
+ // Modification des features
+ ipcMain.handle("set-feature", (event, { key, value }) => {
+ try {
+ if (!FEATURE_WHITELIST.has(key)) {
+ logger.warn(`Rejected feature (not whitelisted): ${key}`);
+ return false;
+ }
+
+ if (configFeatures[key] === value) {
+ return true;
+ }
+
+ configFeatures[key] = value;
+
+ fs.writeFileSync(
+ configFolderPath,
+ JSON.stringify(configFeatures, null, 2),
+ "utf-8"
+ );
+
+ logger.info(`Feature updated: ${key} = ${value}`);
+ return true;
+
+ } catch (err) {
+ logger.error(`set-feature failed (${key}): ${err.message}`);
+ return false;
+ }
+ });
+}
+
+module.exports = { registerIpcHandlers };
\ No newline at end of file
diff --git a/app/pathValidator.js b/app/pathValidator.js
new file mode 100644
index 0000000..aa69107
--- /dev/null
+++ b/app/pathValidator.js
@@ -0,0 +1,32 @@
+const fs = require("fs");
+const path = require("path");
+const { logger } = require("../server/logger");
+
+let _defaultPath = null;
+
+function getDefaultDownloadPath() {
+ if (!_defaultPath) {
+ const { defaultDownloadFolder } = require("../server/helpers/path.helpers");
+ _defaultPath = defaultDownloadFolder;
+ }
+ return _defaultPath;
+}
+
+function validateDownloadPath(userPath) {
+ const { isSafePath } = require("../server/helpers/validation.helpers");
+
+ if (!userPath) return getDefaultDownloadPath();
+
+ try {
+ const resolved = fs.realpathSync(path.resolve(userPath));
+ if (!isSafePath(resolved)) {
+ throw new Error("Path not allowed: system folders are blocked!");
+ }
+ return resolved;
+ } catch (err) {
+ logger.error(`Invalid download path: ${userPath} — ${err.message}`);
+ throw new Error(`Invalid or inaccessible path: ${err.message}`);
+ }
+}
+
+module.exports = { validateDownloadPath, getDefaultDownloadPath };
\ No newline at end of file
diff --git a/app/windowManager.js b/app/windowManager.js
new file mode 100644
index 0000000..4b4c273
--- /dev/null
+++ b/app/windowManager.js
@@ -0,0 +1,50 @@
+const { BrowserWindow, app } = require("electron");
+const path = require("path");
+const { logger } = require("../server/logger");
+const { configFeatures } = require("../config");
+const config = require("../config");
+
+let mainWindow = null;
+
+async function createMainWindow() {
+ if (mainWindow) {
+ logger.warn("Window already exists, no new creation!");
+ return mainWindow;
+ }
+
+ mainWindow = new BrowserWindow({
+ title: `Freedom Loader ${config.version}`,
+ width: 750,
+ height: 800,
+ minWidth: 750,
+ minHeight: 800,
+ frame: !configFeatures.customTopBar,
+ devTools: !app.isPackaged,
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, "../preload.js"),
+ },
+ });
+
+ try {
+ await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
+ logger.info("Window loaded");
+ } catch (err) {
+ logger.error("Window loading error:", err);
+ throw err;
+ }
+
+ mainWindow.on("closed", () => {
+ logger.info("Main window closed");
+ mainWindow = null;
+ });
+
+ return mainWindow;
+}
+
+function getMainWindow() {
+ return mainWindow;
+}
+
+module.exports = { createMainWindow, getMainWindow };
\ No newline at end of file
diff --git a/app/ytDlpUpdater.js b/app/ytDlpUpdater.js
new file mode 100644
index 0000000..90b400b
--- /dev/null
+++ b/app/ytDlpUpdater.js
@@ -0,0 +1,12 @@
+const { execFile } = require("child_process");
+const { logger } = require("../server/logger");
+
+function updateYtDlp(ytDlpPath) {
+ logger.info("yt-dlp update check starting...");
+ execFile(ytDlpPath, ["-U"], (err, stdout) => {
+ if (err) logger.warn("yt-dlp update failed (continuing):", err.message);
+ else logger.info(`yt-dlp update: ${stdout.trim()}`);
+ });
+}
+
+module.exports = { updateYtDlp };
\ No newline at end of file
diff --git a/main.js b/main.js
index 58621c9..6041f89 100644
--- a/main.js
+++ b/main.js
@@ -1,301 +1,72 @@
-const config = require("./config.js");
-const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
-const path = require("path");
-const fs = require("fs");
+process.on("uncaughtException", (err) => {
+ console.error("Uncaught exception:", err);
+ app.quit();
+});
-const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
-const { AutoUpdater } = require("./server/update.js");
-const { configFeatures } = require("./config.js");
-const { startRPC, stopRPC} = require("./server/discordRPC");
+process.on("unhandledRejection", (reason) => {
+ console.error("Unhandled rejection:", reason);
+ app.quit();
+});
-let mainWindow;
-const logsFolderPath = logDir;
+const { app } = require("electron");
-const basePath = config.localMode
- ? path.join(__dirname )
- : path.join(path.dirname(process.execPath), "resources");
+const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
+const { AutoUpdater } = require("./app/autoUpdater");
+const { startRPC, stopRPC } = require("./app/discordRPC");
-const configFolderPath = path.join(basePath, "config" ,"config.json");
+const { configFeatures } = require("./config");
+const config = require("./config");
-// Default download path (centralized, lazy loaded)
-let defaultDownloadPath;
+const { checkNativeDependencies } = require("./app/dependencyCheck");
+const { updateYtDlp } = require("./app/ytDlpUpdater");
+const { createMainWindow, getMainWindow } = require("./app/windowManager");
+const { registerIpcHandlers } = require("./app/ipcHandlers");
-app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
+app.setAppUserModelId("com.masteracnolo.freedomloader");
app.disableHardwareAcceleration();
-ipcMain.handle("version", () => config.version);
-
-// Gestion single instance
-const gotLock = app.requestSingleInstanceLock();
-
-// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
-function checkNativeDependencies() {
- // Import centralized paths after app initialization
- const { binaryPaths } = require("./server/helpers/path.helpers.js");
-
- const deps = [
- { name: "yt-dlp.exe", path: binaryPaths.ytDlp },
- { name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
- { name: "ffprobe.exe", path: binaryPaths.ffprobe },
- { name: "deno.exe", path: binaryPaths.deno },
- ];
- const missing = deps.filter(dep => !fs.existsSync(dep.path));
- let errorMsg = "";
- if (missing.length > 0) {
- const missingList = missing.map(dep => dep.name).join(", ");
- logger.error(`Missing dependencies: ${missingList}`);
- errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
- }
- if (errorMsg) {
- app.whenReady().then(() => {
- dialog.showErrorBox(
- "Missing dependencies",
- `${errorMsg}\n\nThe application will now exit. Try to reinstall`
- );
- app.quit();
+if (!config.localMode) {
+ const gotLock = app.requestSingleInstanceLock();
+ if (gotLock) {
+ app.on("second-instance", () => {
+ logger.info("New instance detected, closing older...");
+ getMainWindow()?.destroy();
});
- return false;
- }
- return true;
-}
-
-if(!config.localMode){
- if (!gotLock) {
- // Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
- // Ici la nouvelle instance continue normalement
- } else {
- if (!checkNativeDependencies()) {
- // Arrêt déjà géré dans la fonction
- } else {
- app.on("second-instance", () => {
- // La vieille instance se ferme
- if (mainWindow) {
- logger.info("New Instance Detected, closing the older...");
- mainWindow.destroy();
- mainWindow = null;
- }
- });
- }
- }
-}
-// Création fenêtre principale
-async function createMainWindow() {
- if (mainWindow) {
- logger.warn("Window already exists, no new creation!");
- return;
- }
-
- mainWindow = new BrowserWindow({
- title: `Freedom Loader ${config.version}`,
- width: 750,
- height: 800,
- minWidth: 750,
- minHeight: 800,
- frame: !configFeatures.customTopBar,
- devTools: `${app.isPackaged ? false : true}`,
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
- preload: path.join(__dirname, "preload.js"),
- },
- });
-
- try {
- await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
- logger.info("Window Loaded");
- } catch (err) {
- logger.error("Window Loading Error:", err);
- }
-
- mainWindow.on("closed", () => {
- logger.info("Main Window Closed");
- mainWindow = null;
- });
-}
-
-function validateDownloadPath(userPath) {
- const { isSafePath } = require("./server/helpers/validation.helpers.js");
-
- // Lazy load default path
- if (!defaultDownloadPath) {
- const { defaultDownloadFolder } = require("./server/helpers/path.helpers.js");
- defaultDownloadPath = defaultDownloadFolder;
- }
-
- if (!userPath) return defaultDownloadPath;
-
- try {
- // Canonical resolution and symlink following
- const resolved = fs.realpathSync(path.resolve(userPath));
-
- // Use the same validation as backend (allows all drives except system folders)
- if (!isSafePath(resolved)) {
- throw new Error("Path not allowed: system folders are blocked!");
- }
-
- return resolved;
- } catch (err) {
- logger.error(`Invalid download path: ${userPath} - ${err.message}`);
- throw new Error(`Invalid or inaccessible path: ${err.message}`);
}
}
-
-// IPC
-ipcMain.handle("select-download-folder", async () => {
- const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
- if (result.canceled) {
- logger.info("Folder selection cancelled by user");
- return null;
- }
- if (result.filePaths.length > 0) {
- const selectedPath = result.filePaths[0];
- try {
- const validatedPath = validateDownloadPath(selectedPath);
- logger.info(`Folder selected and validated: ${validatedPath}`);
- return validatedPath;
- } catch (err) {
- logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
- throw err; // Propagate error to UI
- }
- }
- return null;
-});
-
-ipcMain.handle("validate-download-path", (event, userPath) => {
- return validateDownloadPath(userPath);
-});
-
-
-ipcMain.handle("get-default-download-path", () => {
- if (!defaultDownloadPath) {
- const { defaultDownloadFolder } = require("./server/helpers/path");
- defaultDownloadPath = defaultDownloadFolder;
- }
- return defaultDownloadPath;
-});
-
-ipcMain.on("set-progress", (event, percent) => {
- if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
-});
-
-// Topbar window controls
-ipcMain.on("window-minimize", () => {
- if (mainWindow) mainWindow.minimize();
-});
-
-// Toggle Maximize -> UnMaximize
-ipcMain.on("window-maximize", () => {
- if (mainWindow) {
- if (mainWindow.isMaximized()) {
- mainWindow.unmaximize();
- } else {
- mainWindow.maximize();
- }
- }
-});
-
-ipcMain.on("window-close", () => {
- if (mainWindow) mainWindow.close();
-});
-
-// Topbar custom actions
-ipcMain.on("open-devtools", () => {
- if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' });
-});
-ipcMain.on("open-logs", () => {
- if (logsFolderPath) shell.openPath(logsFolderPath);
-});
-ipcMain.on("open-website", () => {
- shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html");
-});
-ipcMain.on("open-wiki", () => {
- shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
-});
-ipcMain.on("open-config", () => {
- if (configFolderPath) shell.openPath(configFolderPath);
-});
-
-
-// App ready
app.whenReady().then(async () => {
logSessionStart();
- logger.info("App Ready, Server Express starting...");
- const serverPath = path.join(__dirname, "server", "server.js")
+ if (!config.localMode && !checkNativeDependencies()) return;
- const expressServer = require(serverPath);
+ const { userYtDlp } = require("./server/helpers/path.helpers");
+ updateYtDlp(userYtDlp);
try {
- await expressServer.startServer();
- logger.info("Express Server Started");
+ await require("./server/server").startServer();
- ipcMain.handle("features", () => {
- return configFeatures;
- });
-
- const featureWhitelist = new Set([
- "autoUpdate",
- "discordRPC",
- "customTopBar",
- "autoCheckInfo",
- "addThumbnail",
- "addMetadata",
- "verboseLogs",
- "autoDownloadPlaylist",
- "customCodec"
- ]);
-
-
- ipcMain.handle("set-feature", (event, { key, value }) => {
- try {
- if (!featureWhitelist.has(key)) {
- logger.warn(`Rejected feature (not whitelisted): ${key}`);
- return false;
- }
-
- // optionnel mais propre
- if (configFeatures[key] === value) {
- return true;
- }
-
- configFeatures[key] = value;
-
- fs.writeFileSync(
- configFolderPath,
- JSON.stringify(configFeatures, null, 2),
- "utf-8"
- );
-
- logger.info(`Feature updated: ${key} = ${value}`);
- return true;
-
- } catch (err) {
- logger.error(`set-feature failed (${key}): ${err.message}`);
- return false;
- }
- });
-
-
- if (configFeatures.discordRPC) startRPC(); // Discord RPC
+ registerIpcHandlers(getMainWindow);
await createMainWindow();
- if (configFeatures.autoUpdate) AutoUpdater(mainWindow); // Auto Update
+ if (configFeatures.discordRPC) startRPC();
+
+ if (configFeatures.autoUpdate) AutoUpdater(getMainWindow());
} catch (err) {
- logger.error("Window or Server error :", err);
+ logger.error("Boot error:", err);
app.quit();
}
});
app.on("window-all-closed", () => {
- logger.info("Shuting Down App...");
+ logger.info("Shutting down...");
app.quit();
});
app.on("before-quit", async () => {
await stopRPC();
- logger.info("All Services Stopped. Have a nice day!")
- logSessionEnd();
-});
+ logger.info("All services stopped. Have a nice day!");
+ logSessionEnd();
+});
\ No newline at end of file
diff --git a/server/server.js b/server/server.js
index 3992fd9..a31e6ec 100644
--- a/server/server.js
+++ b/server/server.js
@@ -2,30 +2,19 @@ const express = require("express");
const path = require("path");
const { logger, logSessionEnd } = require("./logger");
const config = require("../config");
-const { execFile } = require("child_process");
-const { userYtDlp } = require("./helpers/path.helpers");
-const { rateLimite } = require("./helpers/rateLimit.helpers")
+const { rateLimite } = require("./helpers/rateLimit.helpers");
const app = express();
-app.use(express.json());
-
-// Update YT-DLP on Startup
-execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
- if (err) logger.warn("yt-dlp update error:", err);
- else logger.info(`Update yt-dlp : ${stdout}`);
-});
-
// Middlewares
+app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, "../public")));
// Routes
app.use("/download", require("./routes/download.route"));
-app.use("/info", require("./routes/info.route"));
-
-// When we get API Root, it return the front pages.
-app.get("/", rateLimite ,(req, res) => {
+app.use("/info", require("./routes/info.route"));
+app.get("/", rateLimite, (req, res) => {
res.sendFile(path.join(__dirname, "../public/index.html"));
});
@@ -37,19 +26,19 @@ async function startServer() {
});
server.on("error", (err) => {
- logger.error("Express Server Error :", err);
+ logger.error("Express server error:", err);
reject(err);
});
const gracefulExit = () => {
logSessionEnd();
server.close(() => {
- logger.info("Express Server close cleanly.");
+ logger.info("Express server closed cleanly.");
process.exit();
});
};
- process.on("SIGINT", gracefulExit);
+ process.on("SIGINT", gracefulExit);
process.on("SIGTERM", gracefulExit);
});
}
From 9b4b0306c77d391b64686eca0d67690a4fa92e0e Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 16:33:58 +0200
Subject: [PATCH 08/44] fix: move config to AppData to prevent settings loss on
update, add config.dev.json for dev environment
---
.gitignore | 2 ++
app/ipcHandlers.js | 12 ++-----
config.js | 38 +++++++++++++++------
config/{config.json => config.default.json} | 0
package.json | 7 ++--
5 files changed, 36 insertions(+), 23 deletions(-)
rename config/{config.json => config.default.json} (100%)
diff --git a/.gitignore b/.gitignore
index b044080..3c5cf2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,7 @@
/ressources/ffprobe.exe
/ressources/deno.exe
+config/config.dev.json
+
logs/*.json
logs/*.log
\ No newline at end of file
diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js
index 8e953a0..556f1c3 100644
--- a/app/ipcHandlers.js
+++ b/app/ipcHandlers.js
@@ -1,18 +1,10 @@
const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs");
-const path = require("path");
const { logger, logDir } = require("../server/logger");
-const { configFeatures } = require("../config");
+const { configFeatures, featuresPath } = require("../config");
const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
-const configFolderPath = path.join(
- config.localMode
-
- ? path.join(__dirname, "..")
- : path.join(path.dirname(process.execPath), "resources"), "config", "config.json"
-);
-
const FEATURE_WHITELIST = new Set([
"autoUpdate",
"discordRPC",
@@ -25,6 +17,8 @@ const FEATURE_WHITELIST = new Set([
"customCodec",
]);
+const configFolderPath = featuresPath;
+
function registerIpcHandlers(getMainWindow) {
// Infos générales
diff --git a/config.js b/config.js
index 4a40e97..3e8a43d 100644
--- a/config.js
+++ b/config.js
@@ -5,23 +5,39 @@ const path = require("path");
const localMode = !app.isPackaged;
-// Change path for JSON file
-const featuresPath = path.join(path.join(`${localMode ? __dirname : process.resourcesPath}`,"config/" , "config.json"));
+function resolveConfigPath() {
+ if (localMode) {
+ const devConfigPath = path.join(__dirname, "config", "config.dev.json");
+ if (!fs.existsSync(devConfigPath)) {
+ const defaultConfigPath = path.join(__dirname, "config", "config.default.json");
+ fs.copyFileSync(defaultConfigPath, devConfigPath);
+ }
+ return devConfigPath;
+ }
-let features = {};
+ const userConfigPath = path.join(app.getPath("userData"), "config.json");
+ if (!fs.existsSync(userConfigPath)) {
+ const defaultConfigPath = path.join(process.resourcesPath, "config", "config.default.json");
+ fs.copyFileSync(defaultConfigPath, userConfigPath);
+ }
+
+ return userConfigPath;
+}
+
+const featuresPath = resolveConfigPath();
function loadFeatures() {
- const raw = fs.readFileSync(featuresPath, "utf-8");
- features = JSON.parse(raw);
- return features;
+ const raw = fs.readFileSync(featuresPath, "utf-8");
+ return JSON.parse(raw);
}
const configFeatures = loadFeatures();
module.exports = {
- version: packageJson.version,
- applicationPort: "8787",
- localMode,
- DiscordRPCID: "1410934537051181146",
- configFeatures
+ version: packageJson.version,
+ applicationPort: "8787",
+ localMode,
+ DiscordRPCID: "1410934537051181146",
+ configFeatures,
+ featuresPath
}
\ No newline at end of file
diff --git a/config/config.json b/config/config.default.json
similarity index 100%
rename from config/config.json
rename to config/config.default.json
diff --git a/package.json b/package.json
index dfff853..a9e324a 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,8 @@
],
"files": [
"**/*",
- "node_modules/**/*"
+ "node_modules/**/*",
+ "!config/config.dev.json"
],
"directories": {
"buildResources": "build"
@@ -77,8 +78,8 @@
"to": "binaries/deno.exe"
},
{
- "from": "config/config.json",
- "to": "config/config.json"
+ "from": "config/config.default.json",
+ "to": "config/config.default.json"
}
],
"win": {
From 9f88fe19c36163dfc077d02e010e39aa4d9d39cc Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 16:37:35 +0200
Subject: [PATCH 09/44] Fix: Theme Saving
---
public/script/settingsPanel.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/script/settingsPanel.js b/public/script/settingsPanel.js
index 50fd402..40080fd 100644
--- a/public/script/settingsPanel.js
+++ b/public/script/settingsPanel.js
@@ -27,6 +27,7 @@ async function loadSettings() {
}
el.addEventListener("change", () => {
+ if (key === "theme") return;
let value = el.type === "checkbox" ? el.checked : el.value;
window.electronAPI.setFeature(key, value);
});
From 23d11d893376ceeae013891adbbdbf067f601b57 Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:10:51 +0200
Subject: [PATCH 10/44] Feat: Custom Theme features and clean the old one
---
.gitignore | 2 +
app/ipcHandlers.js | 7 +-
app/themeManager.js | 190 +++++++++++++++++++++++++
config/config.default.json | 3 +-
main.js | 8 ++
package-lock.json | 90 +++++++++++-
package.json | 5 +
preload.js | 3 +-
public/assets/images/Chirac.png | Bin 3405391 -> 0 bytes
public/assets/images/Drift.jpg | Bin 990539 -> 0 bytes
public/assets/images/NF.jpg | Bin 142442 -> 0 bytes
public/assets/images/Songbird.webp | Bin 634822 -> 0 bytes
public/assets/images/Spicy.png | Bin 5505583 -> 0 bytes
public/script/customthemes.js | 145 ++++++++++++-------
public/styles/styles.css | 3 -
public/styles/theme/chirac-theme.css | 108 --------------
public/styles/theme/dark-theme.css | 54 -------
public/styles/theme/drift-theme.css | 66 ---------
public/styles/theme/fanatic-theme.css | 54 -------
public/styles/theme/light-theme.css | 53 -------
public/styles/theme/neon-theme.css | 64 ---------
public/styles/theme/nf-theme.css | 63 --------
public/styles/theme/songbird-theme.css | 63 --------
public/styles/theme/spicy-theme.css | 64 ---------
public/styles/theme/themeimport.css | 9 --
public/styles/variables.css | 3 +
26 files changed, 400 insertions(+), 657 deletions(-)
create mode 100644 app/themeManager.js
delete mode 100644 public/assets/images/Chirac.png
delete mode 100644 public/assets/images/Drift.jpg
delete mode 100644 public/assets/images/NF.jpg
delete mode 100644 public/assets/images/Songbird.webp
delete mode 100644 public/assets/images/Spicy.png
delete mode 100644 public/styles/theme/chirac-theme.css
delete mode 100644 public/styles/theme/dark-theme.css
delete mode 100644 public/styles/theme/drift-theme.css
delete mode 100644 public/styles/theme/fanatic-theme.css
delete mode 100644 public/styles/theme/light-theme.css
delete mode 100644 public/styles/theme/neon-theme.css
delete mode 100644 public/styles/theme/nf-theme.css
delete mode 100644 public/styles/theme/songbird-theme.css
delete mode 100644 public/styles/theme/spicy-theme.css
delete mode 100644 public/styles/theme/themeimport.css
diff --git a/.gitignore b/.gitignore
index 3c5cf2c..825f0c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@
/ressources/ffprobe.exe
/ressources/deno.exe
+/theme/**
+
config/config.dev.json
logs/*.json
diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js
index 556f1c3..6efa381 100644
--- a/app/ipcHandlers.js
+++ b/app/ipcHandlers.js
@@ -1,7 +1,9 @@
const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs");
+const path = require("path");
const { logger, logDir } = require("../server/logger");
-const { configFeatures, featuresPath } = require("../config");
+const { configFeatures, featuresPath } = require("../config");
+const { getThemes } = require("./themeManager");
const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
@@ -15,6 +17,7 @@ const FEATURE_WHITELIST = new Set([
"verboseLogs",
"autoDownloadPlaylist",
"customCodec",
+ "theme"
]);
const configFolderPath = featuresPath;
@@ -72,6 +75,8 @@ function registerIpcHandlers(getMainWindow) {
);
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
+ ipcMain.handle("get-themes", () => getThemes());
+
// Modification des features
ipcMain.handle("set-feature", (event, { key, value }) => {
try {
diff --git a/app/themeManager.js b/app/themeManager.js
new file mode 100644
index 0000000..4d2b4c6
--- /dev/null
+++ b/app/themeManager.js
@@ -0,0 +1,190 @@
+const fs = require("fs");
+const path = require("path");
+const { logger } = require("../server/logger");
+const JSZip = require("jszip");
+
+const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
+const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"];
+const ALLOWED_IMAGE_NAMES = ["cover", "background"];
+
+const REQUIRED_KEYS = [
+ ["meta", "name"],
+ ["meta", "author"],
+ ["meta", "version"],
+ ["meta", "formatVersion"],
+ ["style", "colors", "background"],
+ ["style", "colors", "text", "default"],
+ ["style", "form", "button", "background"],
+ ["style", "progressBar", "fill"],
+];
+
+function getNestedValue(obj, keys) {
+ return keys.reduce((acc, key) => acc?.[key], obj);
+}
+
+function validateThemeJson(json) {
+ for (const keyPath of REQUIRED_KEYS) {
+ if (getNestedValue(json, keyPath) === undefined) {
+ return { valid: false, reason: `Missing key: ${keyPath.join(".")}` };
+ }
+ }
+ return { valid: true };
+}
+
+let cachedThemes = null;
+let themeFolderPath = null;
+
+function setThemeFolderPath(folderPath) {
+ themeFolderPath = folderPath;
+}
+
+async function loadThemesFromFolder() {
+ const themes = [];
+
+ if (!fs.existsSync(themeFolderPath)) {
+ logger.warn(`Theme folder not found: ${themeFolderPath}`);
+ return themes;
+ }
+
+ const files = fs.readdirSync(themeFolderPath);
+
+ for (const file of files) {
+ const filePath = path.join(themeFolderPath, file);
+ const themeId = path.basename(file, path.extname(file));
+
+ // ZIP
+ if (file.endsWith(".zip")) {
+ const theme = await loadThemeFromZip(filePath, themeId);
+ if (theme) themes.push(theme);
+ continue;
+ }
+
+ // Dossier décompressé
+ if (fs.statSync(filePath).isDirectory()) {
+ const theme = await loadThemeFromFolder(filePath, themeId);
+ if (theme) themes.push(theme);
+ continue;
+ }
+ }
+
+ return themes;
+}
+
+async function loadThemeFromZip(zipPath, themeId) {
+ try {
+ const zipBuffer = fs.readFileSync(zipPath);
+ const zip = await JSZip.loadAsync(zipBuffer);
+
+ const jsonFile = Object.keys(zip.files).find(f => f.endsWith(".theme.json"));
+ if (!jsonFile) {
+ logger.warn(`Theme ${themeId}: no .theme.json found, skipping`);
+ return null;
+ }
+
+ const jsonContent = await zip.files[jsonFile].async("string");
+ let themeJson;
+ try { themeJson = JSON.parse(jsonContent); }
+ catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
+
+ const validation = validateThemeJson(themeJson);
+ if (!validation.valid) {
+ logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`);
+ return null;
+ }
+
+ let imageData = null;
+ const imageFile = Object.keys(zip.files).find(f => {
+ const ext = path.extname(f).toLowerCase();
+ const name = path.basename(f, ext).toLowerCase();
+ return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
+ });
+
+ if (imageFile) {
+ const imageBuffer = await zip.files[imageFile].async("nodebuffer");
+ if (imageBuffer.length > MAX_IMAGE_SIZE) {
+ logger.warn(`Theme ${themeId}: image too large, ignoring image`);
+ } else {
+ const ext = path.extname(imageFile).toLowerCase().replace(".", "");
+ const mime = ext === "jpg" || ext === "jpeg" ? "jpeg" : ext;
+ imageData = `data:image/${mime};base64,${imageBuffer.toString("base64")}`;
+ }
+ }
+
+ logger.info(`Theme loaded (zip): ${themeId}`);
+ return buildThemeObject(themeId, themeJson, imageData);
+
+ } catch (err) {
+ logger.warn(`Theme ${themeId}: failed to load zip — ${err.message}`);
+ return null;
+ }
+}
+
+async function loadThemeFromFolder(folderPath, themeId) {
+ try {
+ const jsonFile = fs.readdirSync(folderPath).find(f => f.endsWith(".theme.json"));
+ if (!jsonFile) {
+ logger.warn(`Theme ${themeId}: no .theme.json found, skipping`);
+ return null;
+ }
+
+ const jsonContent = fs.readFileSync(path.join(folderPath, jsonFile), "utf-8");
+ let themeJson;
+ try { themeJson = JSON.parse(jsonContent); }
+ catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
+
+ const validation = validateThemeJson(themeJson);
+ if (!validation.valid) {
+ logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`);
+ return null;
+ }
+
+ let imageData = null;
+ const imageFile = fs.readdirSync(folderPath).find(f => {
+ const ext = path.extname(f).toLowerCase();
+ const name = path.basename(f, ext).toLowerCase();
+ return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
+ });
+
+ if (imageFile) {
+ const imageBuffer = fs.readFileSync(path.join(folderPath, imageFile));
+ if (imageBuffer.length > MAX_IMAGE_SIZE) {
+ logger.warn(`Theme ${themeId}: image too large, ignoring image`);
+ } else {
+ const ext = path.extname(imageFile).toLowerCase().replace(".", "");
+ const mime = ext === "jpg" || ext === "jpeg" ? "jpeg" : ext;
+ imageData = `data:image/${mime};base64,${imageBuffer.toString("base64")}`;
+ }
+ }
+
+ logger.info(`Theme loaded (folder): ${themeId}`);
+ return buildThemeObject(themeId, themeJson, imageData);
+
+ } catch (err) {
+ logger.warn(`Theme ${themeId}: failed to load folder — ${err.message}`);
+ return null;
+ }
+}
+
+function buildThemeObject(themeId, themeJson, imageData) {
+ return {
+ id: themeId,
+ name: themeJson.meta.name || themeId,
+ author: themeJson.meta.author || "Unknown",
+ version: themeJson.meta.version,
+ subtitle: themeJson.meta.subtitle || "",
+ style: themeJson.style,
+ image: imageData,
+ };
+}
+
+async function initThemes(folderPath) {
+ setThemeFolderPath(folderPath);
+ cachedThemes = await loadThemesFromFolder();
+ logger.info(`${cachedThemes.length} theme(s) loaded`);
+}
+
+function getThemes() {
+ return cachedThemes ?? [];
+}
+
+module.exports = { initThemes, getThemes };
\ No newline at end of file
diff --git a/config/config.default.json b/config/config.default.json
index d73ef86..328a2b4 100644
--- a/config/config.default.json
+++ b/config/config.default.json
@@ -11,5 +11,6 @@
"logSystem": true,
"outputTitleCheck": true,
"downloadSystem": true,
- "notifySystem": true
+ "notifySystem": true,
+ "theme": "dark"
}
\ No newline at end of file
diff --git a/main.js b/main.js
index 6041f89..9be3e35 100644
--- a/main.js
+++ b/main.js
@@ -9,6 +9,7 @@ process.on("unhandledRejection", (reason) => {
});
const { app } = require("electron");
+const path = require("path");
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
const { AutoUpdater } = require("./app/autoUpdater");
@@ -16,6 +17,7 @@ const { startRPC, stopRPC } = require("./app/discordRPC");
const { configFeatures } = require("./config");
const config = require("./config");
+const { initThemes } = require("./app/themeManager");
const { checkNativeDependencies } = require("./app/dependencyCheck");
const { updateYtDlp } = require("./app/ytDlpUpdater");
@@ -48,6 +50,12 @@ app.whenReady().then(async () => {
registerIpcHandlers(getMainWindow);
+ const themeFolderPath = config.localMode
+ ? path.join(__dirname, "theme")
+ : path.join(process.resourcesPath, "theme");
+
+ await initThemes(themeFolderPath);
+
await createMainWindow();
if (configFeatures.discordRPC) startRPC();
diff --git a/package-lock.json b/package-lock.json
index f2d2963..ed318fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "freedom-loader",
- "version": "1.4.7",
+ "version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "freedom-loader",
- "version": "1.4.7",
+ "version": "1.5.0",
"dependencies": {
"chalk": "^4.1.2",
"debug": "^4.4.1",
@@ -14,6 +14,7 @@
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
+ "jszip": "^3.10.1",
"webidl-conversions": "^8.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@@ -1925,9 +1926,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
- "dev": true,
- "license": "MIT",
- "optional": true
+ "license": "MIT"
},
"node_modules/crc": {
"version": "3.8.0",
@@ -3515,6 +3514,12 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -3612,6 +3617,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
"node_modules/isbinaryfile": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
@@ -3736,6 +3747,48 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3758,6 +3811,15 @@
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"license": "MIT"
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
@@ -4447,6 +4509,12 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -4607,6 +4675,12 @@
"node": "^18.17.0 || >=20.5.0"
}
},
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -5039,6 +5113,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
diff --git a/package.json b/package.json
index a9e324a..fd4cb2b 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
+ "jszip": "^3.10.1",
"webidl-conversions": "^8.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@@ -45,6 +46,10 @@
},
"asar": true,
"extraResources": [
+ {
+ "from": "theme",
+ "to": "theme"
+ },
{
"from": "build/confirm-icon.png",
"to": "confirm-icon.png"
diff --git a/preload.js b/preload.js
index e9ca875..9692594 100644
--- a/preload.js
+++ b/preload.js
@@ -7,7 +7,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
getFeatures: () => ipcRenderer.invoke("features"),
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
getVersion: () => ipcRenderer.invoke("version"),
- getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
+ getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path),
+ getThemes: () => ipcRenderer.invoke("get-themes"),
});
// Contrôles de fenêtre et outils custom pour la topbar
diff --git a/public/assets/images/Chirac.png b/public/assets/images/Chirac.png
deleted file mode 100644
index f40b8e78614328f6549db91b3b0907d40086d764..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 3405391
zcmV)hK%>8jP)
?5gT&R+Fq27h5Co2WX&QqOm5LXlbmUr-_DKT3X^-q$r6L
zi`~Vps{DuucYp5~ur=6#-J2cNo$2ZE-o59T!GMj84VeAaU;djrKipxyNVpr}s4_FM
zV1PM@sxmWot11!(nwgoKnY)vzzlRwK05BLpvj4Jbg&CYI1Kka9GbecOT>xNN02o%`
z;cjMdGtdeo%gpbrOu88Wpu*h@W(J^GpaI4z0zjFW9>UDs;0}`R0T@7#RjewQS$`nS
zy$V19Fhdc5xfx;1g29Y{1C~`gGYK;z2@(c3m;p2c%oy$fKp0?pP=mp$NHdaJUIPrS
zbt9AJtg0&L7eKST)^$a!aIgrtIbfN)2pF<@m6NP0^0@^p&Vo3s0I;Z6CPC^=odmVx
zT8I8^07j4mK!9cjda(fZ`@o3O&-NmMB+T?HFz7S!{77iI41}3MAFee6=ZE#?dRpoi
zNm5paF@W&5YkhiLZ!iQX*O!up(8tKyAji#U
zVf#;QKIQ;{Vwt)1-vgx94X(p$V8vPh_udg9%WijOAZajj1Mz(9`O9;T&I4dJLR;wO
zT0{?L$%g&>a1|{~FIYu4fG~Gf!Hghi;Z_E+jOO8)=~~tP5b(t?dMVsFCS~J_O*=J@
zN3yE)zU`?Eu73rvcswY~s?yBe9c;IWVyxhNBjzlSe6bc|6%66AGvz0YjJ2u?>@YFb
zM0aPugPa#5qN+%GxWmooB#?d{8|M0ZgF*nz!d)-u;aY&q>S!gmKL)I&aPz8^nP@r9
zEUUDV5rL|#@)aog1j{OrvVVXcAy;2=xjIw&oA$I;BnqV53oQabW_iSY?|=N~KmOPM
z&;M5efB7$d_iz91Z@*mrc3t{3GgPI!_os0T-`XtQTu}{XdL->QSw>no{v&_I>H`m)
zIA|cuS!M28e{*L+{wj0J#ku=3JlQh6Q$jVHeI5UTQr@{N2xBmoZ*Fl&$tVnmO
zYVQOXSu_}rjdbh?^CJ6oX5aNuX3k18FBZrZp`(iN$g=qp-Kz-D;RGy%aWr%NJHQ_%h_)ZdL;?+>K`7!HjJ4FtdIg8|P*oJ_jFF
z6=pCaY2#q^t_FB`WtvAjCHJiAS1>PDW~~U*O2J^LDw-_Q%#2wwTw?ypJgd+E*fNtv
zGYj_`*CY>YhI?-V!UM;Mj)NtA{=oiZkX1>K;CPaLGPBG)es2zDk!Y9L!2$}Y+AnXo
z1DL@q_jY&LGeO7rH+g#yigE5*>*^PY39J+JJBas-$g#BZ8RMDb;3Q83Z>J7wtz-b1
z>0?ouZ6W4P$}d4y2Ou-UO$R|6L7+0{*#s8>Sgd70wK32DPRQd4I#1|eZm##GUH|?c
z|M~y(_y6#?;=lU0fA`xTKZToleEay&=TsGoZ;wTwiYwg1_ugi<%l@bT`QN?0y*<`y
zplBVf^o7;OXCh|DzedH^dX&zLJa5%nYj9@u=0*%8$jhO`Tp(w!^}~2y*=6inpf7x{VrcpRoCN@nfJ%XbzRrGv;~=|2b-EIM)hGvA~kr9o(lmh`Jl(jP}%UuWo5l#?QVXVxzM|}D6_Pf9M&5u9+uzZ=D
zoA0c_5{$LluQnK_!vKb_U%vkO%hzvTzkU4j>(`Gv*IHq|B2dh%WWg*mBf{M8`zFzG
z)4emVlh@47O)^V21wEq@w($mvRuM`7YcQtX%SbjTR7`(0GI=%{&)Y||BdYr
zGA+E*GomWxIaxG!H?OLvn<~N(5tU_VB;#%eB$rVh*geS7=?)LUt${I$CmoUXUzyt)
z&rG}?r=s_dKej@nShhL?jVpS0-r>qpAF~V9oTIKGOI}j
zA_37@zIk*cs>p79dPMS<#TBL`us2o#l
zOjWvf)|4cbAXtZKnY4ixvF{DQ!T~S|P_RfGmRyHmyqU53!Ss+?D7`f(8tnOY<05om
z3$WLJ?QqdMZk|;XkQ*@50k%YHZ5MmNArJ-+XrYvJlZ^1n+?iy9mJWR89IzyVbhmr&
zs&W%W<|HfA!_h~=6m1QN6(Ef;vz=S`&4386$`x_%{q6gAgDv;c@&aouGu-#qYO^Ug
z`SZ4Ud3b9iYU7KjZOZ$GnS0>Hwta%9e$pFMw37^hBE=e|DrRKq_~?wcW6?o&Yl9m6oXy~MvM&ek%^@VrIe^H_
znFXWK3X?>Ou&b)!nqb2u%$e0IcAgGJ!ufTx2Bl^os&aTvHW^5=DogAbO!-tHll!Bx
z__6N@`|{=OvFz>g%MlJYvx1=l{QU5Rjocu=BmVxMzW@8*zkl8J!>7mJ{OiB+<*@?p
zv`C7Rq`pOL%=ky^qtr$}StP6`c{+p`6$6y-nDH6kP`LpG4zO6sEapxYg@1Np@A~%r
zZnmkz#s2*HdOTKmynR|8_V#JLeR`Puslm;vIt3PD5lU>6?p=DaN{95*yD7#E&r@bj
zQ+`^5MMb-r5sl@v5N<4ac<(arNs!nalu^>gOqzw!1J*?a<)A2K<yW{U))R@^NZl3+B!2(%_dovnUJ1jJpI2~p*XznBHhagqo#gZYWj5g~e91}|Oq#0yn1u)0hrm7B
z=?+gj$mAIaDz`UVQbo8$(zwHx-it6~~inab?qx}sx~VgZ!WPN-E<)KearAK#>_Lt7!L)fBwfm{KLQhhsWFFU;V4U`SFKOZ{Z(5|MGwT
zzy9CefB(ndJ|5SbU6(nO1&Qt!A2YyiXc`X}4WxWN+`w`JSeEAWKav06Km6%`|9|>F
z{vZC&|Cg)`k5xs3Rl$dxabva1U@UKPMC3&^2rI&>+yVE@JQVpvAqsA`*Jr7w;9-DQ
zZuvk%nOV5)`zB4?V!xJQzE5G2Zgh}_W?xZYi2)f>Vk(*)a4KxcYBp-o-NULNE&vdJ
zw5zbw-923BsU~?pu@V(C_1RvrSZJsT%JXMKMEhx3pg;iNWLu
z+D|tD$kGoh)VFiTEPVU?SJr6284qfbx7U#(8)Hp)QLfhy&}VJD{qw_nwV}|J?>CtV
zJ|34Xw;GfQz#|?J$zo=wl*F!dctj+M5YjNG1yPx()Ry~jQWG*1Al$8*UlJc`C;+4z
z0Jo+|4~f>Pqw)2a3eliYFze`!Y9byW4}sKKOJ|G2S)8Sqt)m$N-cL%BmEb$mk<0P8
zexNy71;V{D&9Gtton(r280TL;DQ`nkV`b>
z{inb(Nisa3B;QdajYd@|O<-N$Byx}}_bJ#?p|_cFkYQemz0g4iYv>`cR}|J9F+iP0
zQ#l07L&UMH*rw_vDT0pG@ev+f`J0zBs4{oFq@6-9ZS2{ms8K8wZE%OVg~$;SvuJK!
zsbtn14kNP^ZQMOGOUX>Tk)?H?vs(V%#$0Q-#0
z>`X5>l&hXZFt?Wo$~t6YMZyS)1AIk^xi(nuLiyy8iVF@ve=+p{5xFN78}~~SZ&hjS
zfc2}P2pMe<^_Rc>?Z5upzxf~jr+@#)KmGj6FW-Ln-Ea1t*R`^CWm#8S`_<)zlyAe^
zrHiJiDv+VF6EJ)bShQ%!(#Lb$%=lL8rv*xtxuT3;>{S~DpR#}_){{$2MMih`%7UqY
zN*iLOD(zq+6Fz7hBLoskHdNv=gKDZ%?0?C($9sqyttoGlbO+&Ro}kP2M7U*E2q?^Z
z`9v&zuKQ?r#%aA){Q>k2f-Es>PA2piGA%hxf=z_Y8JR+z3-ekZ{ZIi++?-ivO&(Ql
z;odyj2`a#(xNS=(AP>Pzy_#&QS9^Gwc2?px!hNlEf8159J8fab!o(jNmO)U=?OBk}
z@#f@OE)j+tU}qZ!M+3OE-oL+pe*2^ZuvkU|tOh58=$H$tM&j1mGjw?E2*?*-qsI$#
z5#sK~YBy+nR`m{LTdc}Nmq>M96u7&NQ%v`WE~b)VgurEiAps45D()xk@6=JnS66l<
z%)~K56((*|!{#B4PL&Fo4q)H+(W9tr=_39F@l-1|k1!Ub
z98!tLiY|SdH8Np6C0vzRSrM+YIo`$`P85@f>ToI5o_RbxvjB3Zxr+j5o-_;uv&^kh
zp?xe?SwsZ)X62{1w{&_Qzh=T2fQe6lK2t`qjLc^>+uQx|@$vmV-`=2jSN!GP-0
zfAO3B%P(I^FXHRBZ{NOu*M6+E-X0G{^DX>CzI}RIYdM>*iCCSinxnXP!r1W0;4~Ps
za#tEq5)W0n`C3t##WE;tS5=qKJE8*-q3F1lV}7m`rU6IfAJfB{q@_|Zy!JZ`t{@EzDv4Q
zhB?f_oA`dLWsbem9AIWw!iHjK^WcgHKvc;Fj7T&FRz?=y(o7Z|1D5)!4g@P@U&?B;
zY9K4Uqj#E_SRj$M*x52u;_-Ia(
z+ls6bi=EkpK!R=#Z%8IwNwx&j43uV({bL-x3rJU3Cs@TwN`%oRfzZOnU`VnIt?X$*
zVHI4ujj6^ROi&l
zR3+?KLTA3(m}W9+bR>s7QWDX{GoN~4M|qGU@~TRO1&pK#n5=5_-vfk2%hu>Zhs_hOlHLgc1v|R{CK0oHyED=utLU{fm=W&IVT4%~osQ~5obv!pKY@;CC^ZkL
zP(Khs;I2g>^8+NTHH211u}0
zFi^%}6Ihk$_~FG`XQMEh;lhJ0-&Bs}(K!LDbhI6;+?54j#j?gNXchfPnFVDlQ=6_-
z)I$bJ1C&RYa9IQ=Q@HP4RhfCMrSh2KOcnRNCj(Ztj1XB*?wtVE6%NQ80JwL;LQ3Bj
z0Ga6#HEd~Tf8IH}nF)O7soa?i2AO``?(scUN$W6rEleu=Y8mA-wZKpz5eJJSN
z01)m{uA7@vK5>ZhdGh|_&`Kjq(7(|(5ht~8omih0>%*WE)o(4MuPQp-!&s$2b;VBB
zE_Yns)loO>Dwr>OZ1VTN-v9h{Z@0DL%TJ%L>p>&U*(s`trW!6y1+9}qhzlp6{tc?f
z!J4S8^U8vxZBErR%GotEF^kM%F;k(#iOk}C=gwkRq#&7DM69*k-NOibyuH0WR(P}-
zcS%l3J?C*6pdMyr8X!yr!cpYa&;soPU6v#Mu4A-6!+&~9W?;7zTIGW3v
z{HU4Jr;-v~l%Tq9RPdJ7k;jBY;kI!c+qOZ}ZON3IWzz)>X^9q^WTsiCmihqzZjgYf
z&mi$tq;?
z<*cNcsp2T=Oqm)$sm8Q`9UM4LJ3_rlZ9*iZgYT{e3B@3x8(O>@vZ^j-D_N#geW7q8
zYspHPYda4dC)UOL^CB`Gr3Pl^aFsKU_h=8=!2($|j9EnYl39tG;3zNvqA*0Cj-5f3
za+SRf0@RU&nM4MIcjmAk&(=BscAci39wd}smG304>r#q0f$-yP;WieEh~ZTFkamOt
z=YavhyhU~$zMhhDb2yD3dZ)O;ukkZjpplD~`VA%LJ%g1cL0kw+&Xn^YeR$U}%&
zxP`CGN~XFM!>x*jb|l^iL8ew93pJf6=H^{x>oYdR%LWRP=5;1h2E));MzRYbE~3ku
z!aEygZb3_Sv%-XFwac)o%C)_q(co3QYlHRa?QLBl=OvL%2t0-@AFuIu$9$sJo;T~>M9+BK5
z8)W+t5F}Ej@$|$t?=|5{%hns+BwYsv43ZdGpI{Sh^1lvb4F{?>$y%J#n7Lz2*IYY;
zdU`JQZo!iE_5G%Od%S&$h|DUc@~u#KZgnip4NZ1icMqIsQDttY)MsX3RV(m1jl|T&
zWj9S%$-A4in{O?Z#HSSYQqrr?s~WX8Hb6vl1)+ngA?u)Dj^2~<&Aq-IB?je8!YTD=
zQCJ+#$*5Zcp{#RqJ%Qy@$2RLqj5z^1L
zs#r1kFxsMKkB-oaK3Fjf#j;u-m
z>#-h>>;9|gz>fBwTSKmYvm-~YpZ_!qzZO`t5(RfU40?YKD(5vDjXtwlH$YYvmt
z5kPu6oKU1+SD##ibXciqu4LFJ*VTtlHC_kVp6A^>x?ZSAC+J`{ODaFp5F9pwk2c>a
zz}awOXBh`h^-8)!0_S!XNK|jNi~E_QG~q%UG}VWWLadSw{iOYPVbl5~p1*I3
z4T6&1M~N)Nv$eVP{@Q6%_@4ez)wquVG*34fzC=`}hyaG{IXND|22HlZku$avS|-b+
z(Je5w6PpU+Gb#}v!^6U=YF*d>HX2o+S~JtbXZWami;SrK0`vT{@qRppXj^~MN0(qkTAES>0!%Y_3I;L1tt;d
z_$WOxE6p!timuH}nQ~@=ur=gMX-Y>c;=^gZ>E&%3MG^Qe)0>@SzQJKWOtKi9|*s3x-3Y9xeYC_eJBuH|*
z?|Q#K-X3pCRFyqe!_+SRntKGfcQMV(&GOD_%x1YI?aQhaE6i-)o0aZSmC7)u!>NLM7I~uz?yGjm-;%Wp=DytAEW#x~uB?0CA;M4iRRxkp0x}Pt5>67eZ;&2q
zJs$i1c=N~G<4v`>AHIB6NAkCC@9!TU@4MX19N{k2FiGx2#0t+WgPPv9Y(e3vSpYLE
zkSxQL0@TE>T5O>^VMi@mNp%dHDkwH?o~oypc~ynuaa|E_Km73NFaP2PtVh*n5+7h8xz3-~9CBZ+`N>_;~-*&%b>8`u_g?-YJ$X
zJv;(?OW=xYg<6Z+vWFIszc6=JcH%t2R7xTpJr84wKJJMH=yo8loefsq09UUtS^G{i
ztKmn`LmIk>f>bTRYO^ZW^1uG8fAO#W=6CCgFc_pjqq#9juxQu1aA9W>{_Xv%DtC|T
z@%FZOXI4qVI}$_4>BUr=!dJ0W29{;Yheo%?iU>{u%T~X&cSdcB*BE(6Gia@Gl(C7~
z>j@v7jhEtm2f?%4?O6r*M@m1vMC{tblX~`PDr!XW0;>+3XkBS%g~BgQU`+!e22Z|3
z=R<;{1x6fHhc@LdP&M*YZ570Ms_H2|lZar1%Z<)$Typ1S9do6N0a|b=dZP~95?yN}
z$2_o`T|MnD@NNh9?yhj_x*DvEOZvshT%K=H+840<0$Y};&kad&UtxeZbe-`W&!9j+
zU`bppCqb7KsihP;J}_F-5ycWy*Z7p3d?s%t!zV(hRlIx9bX_VXa6B00#
zp;c3`9z4@F^+dU&(|uV|X!y~JE2Z%o1GqHhftHnRt1RJ1rM(VhL;9|cDn)Wk{~Q@H
zg;z64ar9K{x`Ykf&CT}S@9*#TPLELMs4OBsW~nX
zV3Uh850!$t*SV*LFdI`ePR8CoPHi4Y~~Gp~hA{c(3OZceUWaX1QnLOXKIs
zN>7J_wx(@N0<%@l-b?LV975X(_Q{k;n+fZ0&Cr=38V4o>A{0HPJ!Z)=F!lNyRu+?!RE&5#nXops;)zH=A%W_Pl?dDvP@e_mHSK3!|+JT;`~p>y59
zuTuR2%G~sA+Kcm)W+;}(W%YQFQ2qJ%a2d@Ej2RO3b!UBOu@^)7;esg;sXjlubLp
zclKm9iVAe8b9(X%=Z+JS(!vBEIr*qujuACnO){0>
z8(=7|E6l96WC%=1(&dwP(=BKf4kLD@w&A|FyFIQo4`u2;Y@ZSa#NN$Ju$hQ#)-vW;
zh{t!c)SsDEAgaovcg%{*45}xwIeOLhXbn>)hF7zV$`-<-BJ1>g4J#rAgt~34ICW5i
zPl-Yatap(Z42!L=J@ocJT!*o?E8UT%Ed+>)xf_)(SV!A2nNyFLwinuw9{mw
zSU3u+sXdGvH`QW~Lr;=v_`Xr`=@Dy58qfAZMHp5DQcdqD4Ik)^A~pWQzzDdR(U>&W
zhM75OtzPTU)s~|-R@c1bT2x1oGP0LRH7^>O{ei#I&LPmFJ)H$6hqf
zF+8|)4mMzgc)$72fBNO)<67>Y9ua1H_e7PT!{DI_9&V~JxSDCitBfn`8Nyx7GFl%K
z0_b6hLxepak*DrEh9vj|TK2C`Z0^w10nh$SDRqGTK>{F&)()9mgU~5!6
zygl8wKuL8Oqmzwdg@-=7echAlfDyytf?dyrS=S4O8lGPw*v*SA>glhSms3BJ^~g#P
z8eE`#X#^(O4loUhY{w_(46@W*rJ)3}r$+YYJcD2jm29&k-CC(KSDRDVG`d4Cg@c{5
zTZiq5R(rOFb3nO2wNW}loU&s1A&txntep`Wpo*Q_OnR~T&6>XOIf&WyuyC`iY|J}f
zcF+u7&N61agWhJjZQMV7dHYZP(Br6`#=0IzyIUE{;R*Z^B$pAHR0L8!;U&g
zLf~?AB8s#BjZ|uQnMtqM+Sx80wcl@jRL9(@G3y|g3g?pm9FHxF%2Wy=f_~;$Ka(F8
zAzjYVk~iYl=U4N>lL3gwd^*myrFJYMI)S!3clM8{IQT?b=K*vuW&E5I1M58<95;3&
z)teqK&uOV7;BJ~31$TnE6J37+T63U&CK6ElZ~H(snZW?=y>Wmf~3apn=zoJ%b`LxHnYUjT9z6+?e`dn2zq$&14F}aGM
zjV9Qjc_cMrWbB!@>||E%osY+3@7vw#_@GI2&CpFJ)505Z%nd5=8I9Oh@?+G3ocfx4PN7SIw1G=^sXs6ta+E#vPSz-myEA#x2a6YXqfCF(PDvqE+%`cs0xj6Hy~~$|CQNBG+14&(=a3%q_D-
z`{}rmw6!K&qN-Lbm?85tinZ!`GQuKrZ#0H;f_J8{cSP(=-n*OCG(9=H$_QF>Cm)F9I=>
zkjTt{LC(!!WM)d9WdI)DG;~*R$&pvQ3?I{~*tp+zfXSL}BECJYYXw;z_ISKO=?U%5
zclU_ZQ$6KMcIMK+BBWADgH;wkKA349Fn)Thz3(rd;`8UvsLz@8{;|J)`*vr3eB2)&
zckWvop|fdfCj~)XeiZ;C*5E6XBW>ay%L+hNnAcz+1FpE>C^W|`F-A4-Z}LXsBHi(L
ztZT*RPmeF3KRq5R94j2m%A(ocS!9GpA#rDNQ}tST829K#F|(Zo_*&fwSk=z>am8Q$
z?l=3#{QiD_`~Lpz`}_NSOBS$}hsWNVL>YY!vrw6inkMGm?;n&Ybi+?FaPRyfAPcHV?}rYAMfuh
zW~R9pEg~}0J>*m$k4J9)>DON~;qV`S`0U_~igjh~oGFNn8m31Aovlxo4CfHf40i3f
z3Shd)0#AbcWKk0hrmoO|zNoCL^H!UjVVr!srpBd;(dg9%k_k!2%*HctxWcaQ2=OXw
zdufRy02Hz`X44Mye_UmyW@&d(v+a^k6`N8CxwR=;3~w&+hqce~8K+;8mnD0ZvQ!8m
zOFg4nlM@<`JMRqYIr&J5Zo{5uM70SrN;*cErQ<1RAwsdv*ko&hqdC42;8Bl@){Y$j
zk3ot9L>+d900q$_%buZ#$emrz)MvRVA*vSPDeN{A-&6MmG~J)q#8ls5p~4l;9!yVR
z7cZ5ZH+Kw>StWgma<<-FR3+Dv+^Wl6U4$gj^YF`hrHx<-jl^jx>NbueA{3xCSk{*L
zcY%sUcJmFoJ-YP9=SpH
zC1j}17XxO(&{*t2TqLVmHchNm|C}TaBN5ZO1H@{6zn*Zi8tC~X-GYB|EQ&9($QNra
zKOHTtrcJlYXeEjlW@p%07(+@xV_}P~y
zX%Q5u`sazTn>t1`S7HL2L(fh9NTJ)o<%b@B_-5RuXxpWzTrpgVM2pCFrnP7;&|@BC!*J|d$q
z{aJ1kq{g_q_>WyzHBv2ch`cI&0e~@Rs#g0H6n62_6(4dhNxyR
zHZfZv6cgKP22o`n^~j72~+PSZh=+nTo^Q8ikUAB
zSXWrpLN?z2B*Rsim1|uc8Q5Kb%G6eL+it2i14HkOFBR@cW>eOpnyRVA2HHh4NcN{#
z({!trOC(9MDvK2sx>lgtEq6}eP2|;mvi@SX`YY?8D_%thT#9x8b{K=19Q(=xK1-^D
zafh241$Jgt@d~S=d+0u_BwZ
z09b{s*-C^R@fvyq)Z#!_<%=%v)bz!Qz9AA?^H^A@K!x5Quts|o5wIvlEZcfW#VW$m8C0*Ho^lt!MV#))TD
zfx!-J(2$n4TJP+ov+r=Dz1f=M%UF8s|lAA(0~`aJA3BM6UOsTo#PR8ev={dN~nwp{G4
z4VkM%f^KfEt6Vq@m(LnFX2S~ALhtUqh>*Okv!W^T^)?AvoqAheE@GN~Imu0DTxV08
z^;W>jsuexH$)0u(t=?1hp`z^zoMMv?D#CV|^9W=nJJL}Tp~&NK)M^&dYVkT6sFB{W
z2}k9K-OVtCiWzF(qB)cmMj7ciqqNk~+7T{!((dXXHlzy-ip?3Q7^#`P%V}a9(G>JUy*yj0Jbo4)}
z*jWY+Puhp9Jx@E{IhC5G+aJO?+%J#Oj=5J9V0XiJ+R#=96EW1;)cLN-*P1I4)1k1w
zN3?Ea9aZdnqmMn|#wlRBmMmHPaENyksP~)dDgyi+NGB
zX0vxknnum|6&*P?CF)hR0dqKCl5n%86g$m3#-tVfk7}_R5hnizSoIY<;)Cj$t4xI~
zRxz1lu=4FsOFr(x6($PZ@K+-}$f1_Zj60{df~`gB%C4kll?wdD6;Y_JN`@tYth3)3
z%ak`mlZDiEjJeaVt4cZGv@(r2VsUc>FCzMlIjZ)%@B#@}VUE>NX*M|+(;Lf0aq
zGGWHzW@4j3f`p}2cV_L>Xwc1VPjy1+B8qlH<4v?kbtkdGCLq-`U3qp?+*)O&5^)+@
zEkTsQsvfb{^~n15*RS90FMp$`=We;jfs=P8_eS@B8}VVrE8Le4Bp6xu5Gb$<12^-%?10+Ek$pTAUt%hq+uq^sD-P??6_5-Sj*kr
z@0}jb!u!Yj<8si9sFK*D`U9=e?|t76d;9d(sCVmux0&6U*^~keSmAF!y?y!e0Py~v
zzkd7nao_LnAC;TH&cb!MyEs
z5Ld){T-UWeJ=WXXb*;4`!fl1CE)G8DS2qgUxwneY?HpDtciWjGMT42IOO<<}4IOsg
zcZPc`|IMc_Km7Q1f9&^<{qwKi?t9nAhweDnoEtNDqs!IwjA9^kK;)ljr)P!Cz0F;9
zS@EGfl!In$a>m^&h|EMXT6YpK59kmJPtvHZe`7`1P4H8z@{Sw
z;wiwN8+4ull!W(nPzEuu&&1PsCWTh>Lyduy;26vuosyfmWwGZg^?B|;F;}iC;Bt6F)shTIGNh!&V4ss5wP$MVF_m(A$-Hp>0>gJ+#sm<*9QOmP!I=
ze*R0w*VPLl_7$1Q5pmiu1DuSIFIghc-q^e$vr;IfYOL(9vw6bFtRW0`n8~Bl!en!m
zlc#HK!Qlr6+)9BU|KS8b-nZ^)P=g>z!wK=lF131u-|yeQe|z^>3a<**Gkt-YMnIK!
zNp%U^XdqeDNyFjm_BMBQ<#LMq0J}+91JWbnT5B!$po9?`DC;v03N)AYNE)+hl=X5=
z^&v^LkAp##eu9JOWV_b=OgU8$%+hs}qV}=d$P9Ma`E!cr|Epv@bA6sERiD*4ADFGz
z(bP8iWTtAZW|Lsz(RZ?TRH{1*n
zmQ}SgGs8RycQ~zdO=HQ(?d!Mu*Y~uzK7D!n^!d$K6quPEjbvBo^xWrIDZhipZ;CeM
z=z%8CHzTjZn3AqNuP8nfl9a}vm^-UbB7!w&6sUqS7TpT$P26#LT+7XQt;eo^i+
zVlTUl`DfBt0}x$ql((Jy-2Am%XXp3&MbP~~IBXIcPzd&fZYjdJsYz0fyE&vv)x6gL
z;K}v-?7+|-uN_Z64h7T;cqV-$)HYKZswAEYr~|jqn&(E
zCsSXiAkx#VCMs)PI@zeP+vE480y*sm*z7#+VT#zkT0-_~rfAuiI?;N?`8ZS_FIE
z^aP`auWMb)K0Q|WWWGJe%bIWiaH=-55VXEG0nj~jG~M23R#^yJ5imo~`JP@VH2@en
z)#&FKEU-D%WBIBJn8pGRv810HPDhRG*R}Yp9=@EUR^O`d-wj%(X+>k&r^)z$9Ue$a&^=7dyCv0_$jAy|vWPJWN=V4O}L)TYpsgI7k6y=gIP
zyKDCYg@ECH`+WJ!O7ypprQyl_6^EDBo)<{&y;%%j4)5fJ%*rb+cQ30j_tS2T=aV1j
zVK&9AaWon$QX+N?wrOSL>fZ&H2-0rq3O^n}+ryb1q#D&s`8Ttc4WMBZNqB@rv4#l{
zb9n90u3)0%zTM0n4wcY+NqV*u-86zZzdaU!bG>b5h8x0?i102H8UU*jGdV)kO!v5A
z)u^o_oCH%Mh=4Vj*S$5;-GpoDO+#+xg6gA{&U@FK)f1oFBe%OmuT9m*2OB%tlm{Bx
zQmIc-R6_v_4@^zYd-}SA<>yZlEQ|D0o#WdL|LL~?jMO|^
zT@z)JnN_>4<20;~l17kqYWiB#YF{(utdp0WDZx)?(Lw9@(t|1Wnxq|z=+A0k$0L(z
z!lD;PZ~p$i!@kFtFNC>!jBt(Q-g!MP?|j=iLU(jbtY3+M`oYhp1A7iCZ6Av{|^BwrdnlE;#sNg&)bLxx~PWaaSXqRac@
zzZ2E+pwIb!vMW*56)d#N9<#VuoX;Ph@bCHM
z`YPzYmP$@2YjE~zJZ_QPQ-tj#Y7Wn7b$EWR9&b{bKl1>3_&n`35?eJN)9ZDhi49Q8
zVpzAkMPGx*?jOR_oCs!WtYVXf<0aaSc&1Hz7`3WPluaXeX8`LoB1uccq1*TLxBCc-MyEOz%RV($hLOe*f?OzkmN<{o8-@^vtli>;m#OISJWoRnTAW%%p^1`_qi$HWkK7PZ9<8%{V8j~OcP6*
z2m@4V#Z;YPJxNl=Dc3(~gO~~?sB2x(5l;{MBwREK6+K@C$0Mu#LPVMM54)!DG%^L@
zPD7p=70(PG=?un7#~(FZf|IJ9iFPl=g8DXF2kq4G7KNIeL(pisP^Tfj
zr>xBRsv@WgmRmUY1gaDA`7(jCZ~Jbu+mi7Pt1gK^#}{{&I>|KgyLpWsJKtNV`>$;J
zLKJO};Ct+?m{sOBeL|(RLRb3#_(KgWYDZd65rqNk6jP5R5zIv
z9;ea{XnKWO3Q7Z==C(Be(L&(bdl%sz5#erQcUU2{U}$=SB&wbmPxwmsiM1=qLryqX!@$-G>
z_wOHHzkdICzwf>8d%M#ez6_wN7%Ze|_INzV9z7*kQVd
z`D14~-r|U6z!{ohYdzHQ1#}TT2~*kbu#I?yk}pXbHTK}o63%BYT_*?vLDQ_pR&N{$
z(Rt?QB!E6r#B^7ly2~+p9VH%%f6}N*5-@;YpYFvJ%$EX;WYxSr33j`c0)zHQmmqMO
zR$?Lmi%cL+rqh(rVTOc`STkh1m6^?0m9=QbhA}_HbpK7JC~P&3yR|$*lqNN;RC$h>
z&6thqD;QCNH--k#O+%CK-)qqc}
zMcGHMTYDRRJDE!T&$Ka41E@_OzpiUi+uy{;5>X1MLs}pQLrOj`oqAGpU8(4-`k0Up
zN2iXi+?AC!0!LuP*K;hX!2o8J_5SfuOs6X7YJq)DT)*!GaP$ZF$A?|l@y?h8K?nPQ
zrONC!qXB!c@&er4m$|wEp}S_yI$bK->9y+HpF|8+dBpg-K3rxt)4Ak5rgTT#o_SIq!*im0i_j?34zW7n43g;gkpeaU
zjByaD^$BVnNtJ*Y0Zs6m6cp^V_a%xpQim1WT(n}j`|`(*dTM`BjnLh|y>q#*(1d4Q
zu;FRW;`@94`aKzs&)4FHVkAtAD<+5
z=1D3%aB>Qxi@g~?ZQ}?FMe8hk{)LP{rH%H5Z7R>6?`{ceI$E!Km_W
zl{NQVpi?}#4(by3Q+I65nKr$%cN$Ph7}r|&U0=W5zyH(Muiq`=!AfEq+&s(!oq?z&
zo`s6#E7s$&-Y$QvPPoktx=B~vxhi>h`algG3YGHha
zcdf`=M%$2V(3<|G0Zqv4dM@(uh~aK_g}aD_DW0+`pg@%*Anw(wO%vGlKx9@dx6`KbC&8Jh!xpt=FN
z$xBSkv7AAV;-K2HWMAKCtwXW)Dmp?H;qFlgAgA3HW@{jVwQI5Suj)|qRMD?3%30Z?6X!Rz
ztWW_%9Qh$}(VWCCI=c}n)Vr*J*^r5
zr1uQ*LlF~)HK?bRww!^1ZeG^fO!be?8Ec7~_Pd8`xS0hFxl=-i4h~Pg=&(ExLfC37
zMY}rpV>E)fWHBsuW%Jfur+Eq(~BH?e!(kyG`xc2pxmIjdpMST#}d5Jwfp}KHG18Md5*R{Emue8ml}*u$XA{;
zb@As?HxRYhSp>Tumd)Uc`K_8eDO=Yk00H%1P9o7zR6Svx4cki;;RtM|%ch}uH%oOo
z|7>)dB~fQkJ#obQWJ{S;W*JU!S8(Rs*qPhN3!1rZsz3*oQUZ9*-Y_hS}TpBbP$qa7l#}yq%
z;7aH80}vjDob`jIyEZUVajsX%VuH44ToLde
z_g>fHNB;F+{P1u7vw!{H{`ddy-~Z`P|KmUWhkyH@|0l~0**{!8jZpo2z5MNdpF^T%
zp6aOFw9T0EYMYr(o%Mg{&6VhaFnGhyxj!7I+R7rVL+F^-e3mnOM~jgWi;;c7k&!j+Nd161+7CrnGjsndN>>e8T$O1L5tl@SEu3*|gcXeMc8a{VuDi*8Y
zBiIGQeee6;_uI9U@YODp84sCdrvOaqics>!Tbf1jtYnX>>%iAZLN8Yx{TlZ~{8EdEYC-%-1>|PO`*`i0nqmk}b4LpRZxEe3a4M{rfTW95op>0~cSFtx
z_FL-@HJw{!w7yotv;Z}e@tnpp)4qp9Js<3(L2~1!5x$*R+UayLAUx!;F?f9h5E{`z
z^-mbh%TN$GZJCbFOpxV$HcDz4?vMRN}~(!y=Io8kTazCZTcn?^F`82Bjt
z+D=-hW3IY8k2lEbYRi;%>ihf0wXUfj6X>RVHK}Vc6X_O{fWV;6P_FQVM=af>4uEdv
z$l6uqt$C`-{Pg)#(JRXh94a~7-9nQaK3|t=?OyeGT>IOWnI-pq-~F#7)~rqH@c%
zmQ*V28wWK1r{{~fyGI9|WUjSX%-R4}#ClxL`uushuS!xCVl(%+KX!^Z?z@%`5bYed
zKuu62m~}6YD_V>Y?(UYAZvJ>YuCO1!e7YjcJz}L?Vd4JxxUMU%$X!L|-q*UUfxB_1
zl1x?2i|V1#ET#z`ZfgoQRpk*KX_Tc_f>_s*sf34n$Uaxtk6+$Ce|nJo_{fj<{q@`T
zop)*|w^^q~KAzvkXw2Bjen*ZKtEc66PHPb^nxZ9;y>!~!LLrd=nYGsX{NvlFPoE#x
z`ss%sKfSpb)!FlLe+TG2AEHO#taXw6xNn^fx30biV8ExhPrv`u$JcKknfc}O+o#6@
zna2yd1i1E+KHw=VS4@2NbN6_Hfi0aD7}YZPGLYPQ{0}CV>>}8xE^=ohdA1I?W<5J_
zo3UpwHa8TmevVPt7yz5zR-hA|@u1ZcCUCmjNzn9N&)6*qk2ssomz&objD5Ia4MuW;
ztS*wlkVUsR09k*S*j>6n6kP^xpNa$O#!F9}f**kHjgUD#6WyW*pl8apzh`e1G2UD0
zi9ezKoKI0I-fmIB06j>6X9PM)K>{8RDa&UsNiT?+>cHu0QlrBpM@~5Y^z@Zonwom(IL_bPNfzxl2#aUr<#&c
zY{F++3OIKa@A+z-d=0xZl1=E#xS6HViIOFU+lVCe99N&QiG9OeWB<%ANcE
z*dl@}H=ick>jBbSlFItH@Ar>;g{E%VzCU^mHB()S=N;NG0OKJ1NniWibCM1{x>9TF
z2~r+ckBve9NB{S+GiLTOPF~!J)M-O6(_}(7(_8>?X(CnJ=i0D{G12Df@KlRhO9%(2
z_rpW&I3^lb1!D}^qVLdGtrlO7e*=Izv=s^S=S$gqMQ1vq`0M1m?J=4F_%k+uX2D5$
zI%9p-8i8TSW}*6+PQn5+pddKUaf?d!y*u?e{#MR}L^tr7N2ZFA@%_Gkecx$zJ+3c5
zeYxJ&-kUZbF{p~oT|+bGdNEoLUPpUNQq<6qUvMm`UB%&fIEcSkk2BxV)7uh(JoYQ3uU+-*$eNFKVs`5&CWhmEdhST$=c
zsTN1usY|rml#^*=K;!CpxYo{rnp5IgUme34A&QZ+;_x10@Xw6z5HAg8=Xp6nWd7Y)
zj-DGW*tJ9Z_XAXe4Qq$fRwqk-TH@g4W0aE#$Oz|0>F&j<7TwN5bv5BE&ZO!ZNMJ8!
z0YFzFoDinD1;kU;)2sT@YjIAs{<@6TvV!(kC%-mx^BQ?}Hy@jI>1B|DJcP=wZtLTF
z{qgJj&%b_re=qk3Tei4tdg%>1#$rAz+`|33;`7_%aXHwcPj{F_X9}Z(@R!|E<;+io
zaOgfWmm-$L>G_kxH+L}grc<8iaP0Pq9Xg$_WL9364I}vMQkc57nb=foZ*K@#<-QHH
zb#8hRNm+m?mt+^^(#%P`2wCQs&7o_fMIXDiWku9(=SAjbWv7)IkU7!GnlzHwC{LDY
zJ)~V43MDr*I|uMI>vI@lLbnD0!vSdH4b6qvP*6OFp$)zOs85OPyArjZ-5Uqp$iz^7
zNt#;_rfUt7MN(rDhXZSpz52RqpKvgrYt~_9&Ur^WGj^OJ3zhq^E({J?8dCH;D6Pgi
z6DZpG*i<`ktU>>mW=74(k9GtYWA#2E%5|v_3-hpv8O1WTd_nKL#0ot@qr?k9#6TJl
z!{DQpbrL96_u>^{>sq4rq)ZmMPJNogh^z`F)pG+4%)y-I4^^$v7iPg&rQv>(sK8D&
z#aZQro+NTC06kyGMlo^hjBPQ~Zgl2S!cZt{<1HTNZOnO
zQZ)slYuk!{1x&Sh96a1?t|gj&YBQ)WL)|}m7a0Qp(z^KA)fx<^e$*Hf4mj}Rxg=1P
zYh3Bb0MSJ;n}Wo|56oBh&kNeMxkBIBIX0myk#_QZ^X)pdSh%&k7Vaw?v9i*zvmv%d
z9Lz}qnJCd`Ju8N)0u)g0npkChU7xetbgZv`wt?OA+6NZC4uzOSn0>Q(8NxAj*Z$3O
z{Kq{V(qKTLb;Ww)2Axj~Z{hmEzWKL&VVBBG&t?ARp5L63MTMc5pGCpB+4d5D4R`s=9ux6W>q+om%i@A>LIbo2uL7xBb8e
zm_lNp0nG}NfWldE&R3HWhkh#(Jp|>|2M)L-ME2i?oCw;dX@BcQRad;*8Toh;CB+6?6uPp*eaMLVK`2J`6tRH37=
z;BMaWg40iv#zSHC6Gtyhd+J(uaK>_Ci@uKVHN#-TH>f;pbg2zz>A{u)%h2iYPU!
zs#xLX8q9RYMAvNeu_hJC=5W76&+*{r2x5e;1w@B}Rz;X$dFa|+_4%Qr&&$=b+9pjj
zWJ#R4GLsnGL(1XI}OXmo*RZNf@P}im94t6gfQ0`NyY>^E2%^tgI$}XXS
z{OAco)BMKqlYRTKo6?kSJD4$YRb}Sg1#MU>!sFie$KHHf;VZ2=)jaBlCUFvm$_fRA
zs<*cXItWoIr)tJ)U18xdlW9-lJ=2F&6%2P(nCA4TRzx@Vcx!hqNNXg^!?Vg9E7pd2
za2y)DBu!qiRKVn}J|I;didu+R_s5-?9=_Jf6YD4ERw-r0B8gB?3z?aFtOd)-!)%BX
zwDoq^PF8qCM4$*PF~Te4T&^&3LESiJvhAvL%|t4aTSH9s$=AA8gzh+r6^cmV_IRwY
zzE)hym4;GNztmXQ?rk!)&bMC^0@3Vl^%QcNT&;g+zV8jxNHBhLkRgDx;K
zXg%1u=&@I_rgdoip(5U#2y|QI^i;h#@V=2k(9FzQV9K&C@UsW$H4Nx1snH;CTK{Jd
z!t+X`{yc-&p}jRBAPq$I=zKRvqZfsd7w`jNuh?%`|D(
zSQ;IaTx;Rnx!&@#>bUNHa$vkD8_YDorqeD3;OD)Y?ZLgdDf24bE9`?Hy*fEYL3s(z
zD~f#-2&~E>Zy_pA#Bpuimo_u2E7^UCF0IUpm6e*_>1cTi%&O}9`+M$Oam7+si(l
zkuaKhgy+52dO)30W+1QYBAB^-I!ARl_TH?Db#;o+eZ$qSuiGFsll@#vU{lgQ2g!OE
zxUj1P9QuOSN%rW4J%|$SE?1`S6%9n#`SGa*h;p$>Zb-~&N2?9x2$ChT-C>0mQ2S;+LxKx
zFx%1O&*bkoPrxCu2!jVeDesp)`>{1f4Y_MRRrPyBz#Z3m
zT-SQ6_4c^b8Pa*AfFS#B_`#9tyHs{2$8xICP3t$)Yh6S+5$fc39ZV)el~Gi0&dYK&
z=_=&}yQOwav+ZWqv;7#)*s$)qgH1m10&s-w+0%oR3%19zVJ0z$4o~uRvheO;dn$|-
z-0jc;&5oStW}Ol~`MT!}p$5l?B6#tc-F|p#qP;B`CKgJQPi}P*-;*!Qa>15YZnXZp
z`U=_;lIF6f_WGL6sQ2#PB-+_p7}Qp1YF*V!iBdLGT{GJm>4>>QSI@uS^@l%w{lm}i
zRm=RL-7|w(XJhWKNfJHW+|)Z5p_XNuHEx}^Mm&pEm|hG3h*%QPXdHxhONXs>wQn<^
z+l9~69Jt4^UJOTCx>sVJtkpBrRSbkxGbirTEZf$8pdE)hA+c7Bm+LvtHWh+!ALXn~
z26}LtN;WNzmea!+onbDG8^i5|RQsJ2uQ9*tF)s*fwE^44Tgp0y)QRnxJOP`Z;~5by
zJJ-0gGbsz|!tg#jz{!a5LIibHK#v;ed+X3ctrgHFCq*Ye)FZNw7q9{rDihk%@-vDhW!S*k43!+4c?#b_IYSHp4FzDH){!u
z6r#G0(vf|>t!+z#wn4i4$L7ypzdynsZx4CxfrAyM>1N&d$s%2v9fFK;x=mX-+=7O(
zp4{Ch^$f_C1;C!xw`$H}dr(&QzB65AEj#
zZ1uzQ@9%rR_hf{21!e}fEb5vtGkdI6$*iKW;HeSJ@*3vh(Q^@{1cRgKM7h&BDMX1+
zvE@}q;M5hPfvxS_L1^;pEERg%fVjVbwk92Y&HYXSe`9aPK`KhnFoWvb_)o~t!05K3
zof(dgJ0CG_bZ+C*|IkypQx_4fr72Z2zIrG
z0LRS&=s~G?ewFnP+Nc{QN-6O4fv6&oCoY)=_I61VBmWWLC5ux{?}T6QT&=@eudM3c
zVh>?UpKDMt41VW5t5wtR_f5SMIg{U{?B3cG&ZcT{S;BSsHsDPBZN1*V)q&afUJS
z`00tkVBd$#nYCrpWU?!D@lADQE>EOrAk$eg&1_JmJ-v^SgJCD^_pK^;qBQnm!ln-r
zbi>?zq8noB9Deb%6P}-IGJoJHCAtAf-OQxhzifO_S5>uvHtRj|e|2PnEd%yW)M3+`
zBVND|ed`0r#MztHo)cYlx8vvp1;U@x@a+Hyg5c@@Mtj}o@ehlEwsxuuB05QAZudYB
z2DG=Z|MkE5yI;P3`})h*|M@@s)7$mPR%-8Kat-8;n>H#6TN
zH1@1fs_z6-v1?B_X;1(5Jd!n!V6H28+hu5&hT>6IOgp0!PN7bN4y95E^tT8Mb{%Mf
zVf7P^i;%IPs%(fhJE0M!o|}>37m4thX`@e{^hh@Q!uR!iad<*K!z9uDS9nQ|r1~n-
z-Q27S8n~_9DN3IPkA|+-S|s1!?>o1Eg!P@xZQtFMH~_RNwU|7)y}UGi+MPQR)Bxm<
z?;r2ibv-VhvK7#SpvQ6*%FshKlzv#x+Ukw%yb=w>J6^X~M~kPf%5icHH#EohxYmLG
z99Lfyt2qnl8c+cckWd}wRia%=IkmfQRT0qGME@3Tc;yyT>>SV5SetTC*0(O%GjL4Z
zYuvwCOK2uTrf$ga_MLq0f4yAKKL4|IY}$g64+Dg+z+3{Ppi{-QqRkwZscNxjd1iK6
z$W*YCo!|G?Ts5u0%afFAEY&ln%SZXEnn6RLI&+(w?KRBp^b%1)ugW5-<(7@Fbi#IK
z=I%zlewT?SY!!lsXI9RTZ2$(p_YPlWnR^57sk;Ul;qHDbGh?SGv6dR$U>+jo%xK=1
zomXmebtquYOba)|{zwwJw{!y$k=27wy9K|8JfpHAJZ5a#in#ZlY?21VT81v_SYP+s
zCyivRD0xZpu`Ue-(HK%4dJo_CP1ss%t@W{QQLbI%mr+eZQkt&sQ3gXb7_4?XB%QXo
z<$>>=mFez}$7SX*6R@xA%1o+w;GU@>bh(S)P<=T;T#w7fSFSK1JQi8^-b8$kx9r-U
zNTxrLdt^4@peJ01^B6KYbP+Dr&Yi!M!(tB>+Mr$8cSf*FN_~`Hz`ReUQFTU@U=C*c1WD-JowUAWWIklt
z&gQR<|2agLy5c%!`oQs%dkywMPYxZwJMt8vvj*F0PEzRU79DgqkTm@wifA4qWGWQDQGKB;XntC-x<<^%9H^r$V0ARQ#o0Nt%
zNs%npddvgdLdqWRF8L0QJWX<5$wGD+p&4NflXeTy9}1=#KTz(LRa*j{0;~L5S}ele
zKfZta{;_;LE;SH3Se(1@hVh~Ma9gFgLy{KCxb+^Y8B2B?vAmjCNA%ovx>?zr)@~GP
zBU5+5h}c?6-C-LuRe~h@wynOw;UK0auNnt*Hy43X7~>>$<{l=TT^w75u~He$Nc86d
zzb9b_^L}KKf&?g*6njtqV`fh#_vA2aF$7vD)r6q?j23Bkim9Io>p+)GSyhF2dA3-!
zO?s}IMT7r#k_|QcmZ3Tr2?n{Q;$jZI+e%TD{+O$RW!?EVt_yFE>+{=nEf{Dn)Q2NYoJZ~Sp(lvMB==?!E5e)7X&p*xa=GYEK^WiiTT*j)WPbjcy}?X^_^r?17m
z!C~9BH&G$yZ^iPGr#BqJ>fCZSC<|!bv&j<4uS}}OlU|H7?N%H$vRH(hh29}Mo3rur
z1C2`OsG6(r=ySl4h5-F_Cx5r*qc9puNyVxiMKjFSqwgu36loG+SsFEx3gdb2dV5r6
z-aD&Yv{5001__S-oG@w%s?S;~Pn;+whmy^7=@?{HTE%W0l-K=$uNr;jb5jJK@XsCtU$C*emzU~Bx#&~wH
zqMq%f4NZV`;mc3xd8QVkgYY~mHorCKqNz9CI`EO$dz1L|_RtlC@EdQY9I
z698Ix6@vt<900AxbCA<2*RPsY6IrL^Vja*i_beV4sE9Wp0;XYKY|~QQ7=ZZ{*Plg2
zd-EY>N1}@c4Km&Ak{)=n;?e(gtjpB3ZOYJ8t(s6fjoUB%PjqiMThpDXr#TI0dck7}
zV{b1b7sdq6iyW|F3SE8@uQ^a0+}(4`Y`~Jf+JI_xN9?qSU6I^T~{m|!+s{)^F?OPl0P-EX8qDM0B0Ut|GNC@6M-F@;j0OV
zWUsmHkB~soOU)CFW8B;-d!T<4w@OX53oX-B{PZVOok(dzuQ$W18LREe&21)Xo6nny
z~*RN-#9B
zO8}@xrkFM51laVK*D1Iiw;-~zUC>x*|8&x=RXAYh#R`@E4YRvV)I4Q<=L|i;mLP^U
z*Z2}BMZ((TIPRuD#Y}V9&iURHfwDRn-Oi@qs1T=SVuLGP61rQYh&&VjuUeoc)oH>@m^S
zuRKhyp7{Xu#)1P;IErF#r5OpD#dQ76vGCbqqL?JYUv}k*-_8^uG$AW9=o=|*Lf$#d
zJ_8TWP+}QS%`UY#k;P={W7YeOnE`l~1$cHqV@9+*lisP>=vzQci%AdXd^+Ce)fYIO
ze50-m)-mvIT7t=?pS?9SqP8ht_>m
zri>IoW}XZ}TNmLqvF=cQTHYdcQ3Um1jU|LVio^`__Bj|zxJW>^x3(Rbt`EW7A6Ev;
zq8l?=#aM4E!VEj}{k~~${eqP$25Y2~N^@+#+T+E{EUW3P$|BQ!F2yMtx9p=%+0t$A
ztlI7t?m0x5szmy<6)|`SdvD>EA}f;^Ce&>29yY6@{@%OY_=L`y+Zr^Mvot#l%({y!
zYkNc$ckW~MBoVr$VeQPl(@e8iPs4st5T;U2!su+{?i&b)MWpF3Gu`h=oxUaV5FS>tVWQX=AsDa`I~AGN~VGqZ|o
zE&ae+j-=X^WG3$3JwQ5QX4TkO+?Tn#4xrNMZow&Lp{%YI5eooWdndr$*-~*>a>L-}
zE8MZR@rTbpQerZ1F1d;cthE3z>%=sT{j1!SET(IEK~}6~){V0;L`2?OV-@mFR=I}*
z_bwnw#Bz}LeZyVFU^AS|=K
ziW8ihqVn8=Iob{k9&Rs<0?)0Dta%a40Mf2@+l#OXw3*B~6P1+y_)H3I@fM$1Rc~ij
zfmj|&%d|B^+LpN^2NwYb(+#G*Kicl8TXbLo3<=bVP-0zDui)|yK+BmJD1(|4y0I^`kpIf<2>3}Sd-NP3R0nxnHn
z%78f<*Wz@+8&N#N-E7#2F)gJeC{`R)Y18-LEs@6IB6^r6RXwvY6^qqws@PY!UDsu_
zVmY8doRvccRwr2_g4Bd|fGdKCnjxs#tV9XE^!xCTT{
z($Fb{ni
zh+$`)jdBA78?(bzg3}EvA~C>J{$cKe%PU^OB#F@x>}iyhVcq#dT?2w<1CAHFCADm
z_ae`)pKqKVD%}DEN2Qo~_fP?*IHC>h_qh+gFNJM7qW{!^5bUY5F=xC7HH%-vc%=?u
z^XL9o^LKpyprF6%HTiGvl4yrwC4T+BfB*BhU%%dD
zbOlfJOsg6OmdXbi%se8lhd-9k9IY*(L&KOU&I4Mrkj5gMA=zy7oo2(N*;?Ud8m#5s
zx6hN3()u)&il*q_;mDUKoRTPaX;e~ma=HL!FUGXOib-T#>2}1KFMEKhWQr2PoGdd~e+p
zz#1W)_x*xJWhO(+p!o+I0*aWbxb6#fSgC`%xy&{U>x_ZzLEDd?MEM!gF$ogJW0{RV
zJjnd8VDdwweVT^)!JQ3Qd62}^CGqWJ1NL}ahO#o{=XSV9d9>Mdfd|x_l~dKSSrIhC
zn3_N7fO*UI@U)66Ze
z&8rwnA2XZyP{Nwqh=#JmbDXL?-|GPMy}GKpr%!?r=%V$
zt!^o&_|61k{FqI+Ja^LIrN^a@c7Jz|?3c?DIyP@|UDO&n?`<$CIq=~7-qx00Xy4Vg
zJa0p=s%5k7`^0ksHur9s8!Kh&4XxU61pW0sF9StUd**OXMM9Y*)cTO3
z`}^K(^SN+tpo>1=sySI0oG!zA^%6W`=b*W9WcYJP_N9N;Ha&ZZ&LZ3Rf9&M|(N9O%U`dj)nI`
zCeAZyB#ce6Z6{yo+!AJ}QE0Y@v&L1rIa2J8LTI=9%p#lx(vfLtq4Hq!6Ezw{CY3+X
z%uQy{u)DWezWOPn;pnN`Ozh7HhWW>=D{3HXuBAHxi&c8cM#OYOH^>$XMK{eo9>%Rp
z#dtifop0~&ANRd2+yS@c(?Hs0IYZCPtTN4A9puWb_6+ORj9>F^T3_q>*!$bJ_v^a!
zU_RhT{#+`DEK_geAuHJx|A_??GYDgrTns=m0Rt6w3)UAdDKu176)XCp$LW3Ss5v>O
z2G+GL))1^vbWmd8rfFcgn=-aT6&qA)64Rk{$lawz41(b#Yo1varBaZ4k6i1pf@Ywe
zN0Cp@j=r6!U5zFWA7P5h9;#r8L9P~u15nGr@V{EAQg2F+rdK<#aXl?gfZV)_q|^mm@GV5YpoUQ{oa54<=21tyB`1~Q9IrGLeVA{
zvQAH=S*zz##0_;-?T5xNgrVvN5Nl~TUS(F*wN_=8W->lJ+}yEunyxQmg*(-tU}09-
zB7fa4;@M=4xmisz>mIs)M;UtuC<>v3yJfbn`*E$@kNvU9x?*{RhF%V!!nH0=Ez^wT
zP1+T1VsEpIP1(eXg{Yq2XLlFFl*|o?wULU=wVK+bh(&I}iG6>pbw#ZEzH=wmMV5!}
zO!vNCb?5%{=@YBlCp{uoNci+|r+XcyMz}@e#9^k2!p?F6s>U_7*e@cb$XTU8!9OEsmyTSS$+h;Z2C2`B0M`BAP1rowsV_VtiXgSf6lcBT8=<6
zKR!M_efp$HeG9Dz_bYPW9??~VT_5l75#jD#8Lq)vz~0%hJ))|lvt?3V$jaQiB}ir0
z2&d*{5x%a+2`>d+t%Z9c_s_0UYx*nntoQTx4e4}#CpZztxST&!zICuQ?S
z1$L6$VZ5IV6z3DD;aSfp(GRabpL#R8L@_=bhGO-U2m&KI=?n8npeKM~9?$wx`EH+u
zQn=w34-H`>Yar0nh5~FS{{(dx9>^I?cl0_#WIr3QFJsK
zPc||?Hwa8FgO~c2hD8YuijdQ{cdc}WRlG0_szs%_3sH1K)r8R_DmiRP4bgK73$~MX
zbVUay_!E=y{fsBnN)UZnlPM^6H1;k735#25*X^#R$=ayiQ)|RBY8-V#@XuB
zhgn!OeJ7M^mOCXJi5N7+tW9$Z?^qzK-@ESnHeUe3?7E_ul{;;nTA)Ib9aXy!{y}kl
z1P(PxGg}cm6Q$foO=%e;{UouXfhgx}*W5dx`fmgMffg8f<*rTA%`4ljxd*M09x)T`
zT=%N9-#%`jO_Eg8w&>t-#?B#XC!|UosR#5TF?tr96u*aWzg#&v*5B!&;L3C8g*zP3
zD3mbpRn9vp(0}}o9TL%P%mq_YvCj1RwA-xM@lx`kGPth3m|?BZ|GqBU)l*|TTo?CF
zjz%D?J72^7%MYKgN9^2UCfLj>WC3pG=2RmYT1jo*EoJWA-6J>U;Xu2E^ZFe0Zr#GF
zgc5+dGj%z4_i9vfXF&(Boga$K7DkxbM8fP^DZv1H36l
zg)hnLp*&Sw_qoV?Vt`zV+ON-OSy+4W=H`2%yADxJ)k8@%$7N6c*Q_9W-RvAR<)j8^
zY-xuebn-h&cbsQ4T|hC8xqrFeHlEsj41?4;KBlj(#|w?+HS^P>SY?szE5s>CQ*6;Z
z!|d~k+AlV$@8t|t8(InH3=#zi;u&FM-y(=uy6JY-&0yxHx$)=tTdVx_3^U%q49x(Y
zNnLE_?S+f0zM+W{hquhT*y5S_uPpo1*Y`jE{Qmx)z%n!QkjY~zIWMZy(t&|-UH0ki
zx|Zy>8?!pst_sam@9i+nX*fy0!89s#UX4v%xm{}kOfq~OEaC2Cm|<%09T|Nt>{Nve
z$IQEpzJC{;M@M5k-Cg3IOprKp2OtTrMMTz_v90$IiJ=^*qKdvoS>Yf&T;mh;Mje&e
zE2!j1oaCGof<3+LU<
z1t!>izT%mD4#Cv60<|Th^OKi&reof{HOdkSb2FnoJ4l9K>b(3V7cu2irQ<12S$y6~
zK{B)Z!38xs5XhKHb3wbEqMhj|437!{LpX|{U?)h1PRO70)&Q*Vmoi*?MCyj55v#UW
zpGWboWIz?D-pwkHD2)&5Tma+s`rRiV>(wvzRz8)aB*|#kWU5#BsA3_;j>m5L!Ze*M
zpTPS(y*Ov)SQ}piyLz%gY^q$09A4N3EnH@Yv9AA}8+xCxRKM}`5TZ~dsl?CSxaohvtOT
z85YtHtPHlNnzhYXcVBT*+W9ZN{xr?-H~@WRn_@3ik*c^%5O4q9K>Xx~4RfXLFWv8r
z!ONnJrU`gXP@ZhCsgqg25to>HUlg=klOgHM3fIkGH+N74&6=A9HB<*@5e};9!)HWM
zceDWLhK9y(EYi*Q&b{|_UB=8xxAqVkOoufXsIlJD)1RdzPl#hN=N@$)@SIGr6j6&U
zR-3La@_8@Iaca*jSlwtgBqYbBo`}l1A+KGZc>n5#xtihZ&(EAAM7D??MT^+`Hdw?G
zuPVa6X9rC!P;b@D{`8Az2P?HetSVpjjA?T^B%T0|;KVx?Yo_R?nN^4du5o!tRiLWq
zYP|&v)d5uIq#KN&=EG$65@yPNy=8XPqjuD84=SDl23FR(E+Z?qVBepQnIMZ1R+a8n
zOq;AxB6p=?Y`oD{K^U^xmSmNhT9&l~gcU}6+E%aAN@hu)rOv{=x0%OUyzlStAMYP^
ztyM^Mt|STMJSbt#0$HJ4z>qsNoJVS#%|fO?~o+B%x#QVDwHZ&0{o1d`uMobZN&vplk}tyu(Ha1?>bW-l-wu?^@LAVuwl8g
ztjsK{GIvBU(>yw`;*JS)PhR0*)qg)l8>w(L95k7p|+boZWz
zGpQq=xdCL)gIxgM-@mW5=D4em8InNIV_ngq=z!sY%=UzCrnU>kfL*(&QSep8h?%r*2N483TF%OgHwx1>wz6MYPwjGxc%e
z%V?n*5lj?Q;cD`v>bU~sIEKy@2BOFj0+^=o5zdpN7dcc7ItZO#nJlL9awo-TLhvR1
zvZ3B5x7FtlD8lGE*g6gNbR>-Mce*_W@E3WMbF+P7<;XHl>Q_@WY97JP>(3H6Y~)`o
zYMP?asOxfpGEU>{fyXSurWt@_@y`cGw-{8|QYZy8Hhh|lt*aPghnU;MTRPJtL%XbU
zVrLWgQ!c_uJWc5U@v>V7WLmdAmroo0NtF+OG6@rgd>s6Pv#XsIRnN7m$pUMJG@zFe
zBaG=~E(|5)W-Y#dKDN^0LCppm@g(u&_#1m;`WI#Vvl+80f?*iIBqK5F;gcGJTp)Mu
z2xCt==wqNvY$wglEHbOIOnq&xsXg*SG5mzU(ILls5t|ezJ>1Q^P2bQXAV5qifhlcu
zk~1*4k*_lAT?T|kff+zLEcJt|IT3gG+B1{VFwUCcfXxwA84Mw
z1!Z=v+p*#3>dARe{j8JillEdz1O|Clo>XEExJ_gde4p?PUC~#q)SQ@w$y?h8k?4DJ
zCCPsdVwMgfvveQ49r|ff0&!dgT{qlu(}1e!l^M!;!mCcqlk0SI2KL?r`=;5s!C}1I
zb5j(XoKD&<{_^ePpa1ad-VVCKe7PBVCNN;opY>VQGXu>Yk9d2m$0ZG9is$i}bT}fI
z8AvzUKQz5&FpG$YGfBcN+{|JvclU_y)1W%%#1UG%vFYeAxU{zVfo`1Vd>u?xL!CQ!
zQD1-7sH3_^sW8@!&ZY#}4Yrz4KB=48B8#Y`Ro(_`=K`W{bZgJjzIQIyXh1RgKVY)i
z(IGYZ01mQ}-5;h!nX#Ut`!jTh-t6e`!04|w8GQF>`iQ)HZfsY?ZCH{
p>;$d`dQC8e1VBL_pI1=F6T*S9`|ZDvYhF;FOZ_7Yy86cP6%aujf3BwTdHfv!m!A
zHmT2~1I%Z0RL2Z6;6ThCv(sL)fdIRN!NHyfuSl?9r_FreDSFDkKB}7q+bDFi@+5m~
z(&FpK2gtX_!)YdlLQ%pv$ifZ|na5CwXvGFKn(yGHZYrOkX#QutID@BM_Ys~+l47ZV
z=+k18w!v&l&0RT|n0SJ!P?#B>-GNG(tmEkXGr!&VtbfegO;vG&4@8mX-NxR91hj&l
zCp(l&(to+JnBZ=U?+mnxnnR`e-laOComeZKPKlJn^C|@zN)>AW;fviJV(r2NIHz(%
zv@5~I(@dW}lgw3!5QUe$C${NHoPSQ@=#+M#~r?Uak7nMb4L8q_6CQsEzv1?^M
z7ZUUwLhGza5mf9DCr3TtX-4~jH1jq?*&hQwuZI+Gjs+c8JEtBBUvAE!{%r(E!0(vShQC$+$fgqhD3
zu=A70d38{tu4x=Qr2+IAPx=~)YQR)Csj>8i^D?N?4H=l<>cpqbpl|A;L6=`N7m2>^
znASOEM8s!--agfj%Q
z({i1Auungs`+uEt*qdWQ8IYT12oFg#PWp?dh>J&(sZA2!aZ1_~92C1nNExZ2p#4!N
zfIKzwr&IP=V0$&?xSW-=PT$R*>6BIVoKerTR&@76R+UDJ9W${5OfhJ#vollZzt;M|he21UJ53FaQo$Ku
zJuiq2QP{;lt4wyD2IxHnu`7iivhrHdvo_iOwoi`v%RK+qI_qE6+#AP{KAa>Fh4Sh?
z?DL96Xo5%0r8n%Lz*8zT(GL3Xjs<;H_-u%bq}Mqs*~E|#LS^}KNXpuj6K9tl?N*xB
zk64u@q596fI-+CkIY)5!htS0CS%{y<-^ivqXy$N8e4N_&pEEU3v
z)nF8cp17EIrgwBv8
z$FSFW?EAw;fg62!0p0MiQ&A){-RxoU@$nHMbul;55RR-$%@S8r8Z6|)B=gU^#0H9s
z%u1S?VUi11govAuJHNf(ANK~ZiO32w58qZsXYsBLgPA9{7>&HEKJM%BfRQ`phg0{F
zGGm!XRDr0zl-6urPU;FgM3`E{!c|H{&s5kJ5+a8&HJCT#TVs-@YFe(xs8p6Jo0^Kc
z?+vS_4*9|M?Gp3SX7{@?||P)V|4XVjdS}B$X}hQhl1bc^Px@PE7@?!MrfisTIx0bT(k`
znI)%6q+F#lWd$86QbrMYoS{BiOrfI4q%WAWOj0#Yc*{uoD#`;n{ETp)j2;8UahN)N
zU5jd(DA#un;;@$>C3n(8Qz`P>jyQNSk%?{R51LptxN`iCN&$`b(fuKfn!qjrY6eaT
znaQ*}PZ6T1R$C_)lgu7MPj*_T4B0Jd9P4
zT7Q~cb=+-$pXtVvSry?T4z|~Sl06RRV6)j@8%DxTq4Ol|?X_FB+iR?tq6pP;_8xKx
zA~gz5nml0Y%1|c$oVTG^-*9|-hNgL@FIb*LO1shJv{45EB>MoMKwrPsRqJ4=V=FUt5B2?Km4YD4I@bC)&$emeLYhB9!
zy47#W^jiO_^oZJNXoH8Dela3c59Vld_fB(nc$X+m4VobE{{G?SkGE@pWpAkRdT)x$
zg!1I~BCuk;*Z%m}%9r=PKfS%_H(QM%tUKrHiKX3U?5_cx%`4+YC)Fy|lo7T2F7xKj
z(TPC&3xLyQYnrdn*%oak{Wj0n20(p>a$}qsOPydgF%7Wi?6&M{I2U_yc0O5>2vp$N
zvLnv0$KFgD$exLyiZ-gjtc3-f;DYbIN|xbqU9qke;oW`g0HlqBd34)^c~*i5_zGjk
zZdC;&LP`RPv@kURdlq1UQLJ{ULbHP8+}ru68Okkkzw4svFNfN&Fkx%
zGBi%Wwq`0AYo;#fp8}g3%p$6rIcG``XW;D<*h~t>(~#Pp)Sp}p3G{K0ux4xp3LR#q
z-T?iGTy>wa&Qa%;ZDLRr*-#e4%)OR&toteLJRwFQt%G96LqcNN8YfwuJC7E}6!A58
z^pG#@9uC`VJ&f?K~vM+bs0Bm40NNa|9WZyv?p`lF;Vg%}ss=&K158D{|lU$DhCd
z{OiZwy5!iWAae9X8aC1F@rX~4>+Nzwsy$KPK8y#jsP%jCx~T7!-b%szMS%DZ#viNTePO`5ZAz*_H55O2Wc&G_+v^+!;8ns}AUqg-h8K>Wt*u)*#)oq&Z%scN>
zz%SN)-zD0r`7~&9)yq3KI3!$L@9cZKxoKW%SYd>DSgh%GKF#!ozJsto^@)UP20cS`
z0iWlus_8scwkivz`ns9DXs-J{ss=!f`vZW5$7T^dkI$zihHB4O`?DiP{5)tgP9UTv
zof2~ww!AHf?oew10%I6?VNrF1<{cLQ}l+&B%3E;|@?ADHMExVi_&e|U3BEkkJJ(ea5duh7&z
zfv1k_xduP0CI42`sGT=h8y?j8ZF%1Rll*UQNfv)qb(xVSkOM6Wp9
z>@8MRDK<#LZ_w49uXi2wI0C{{x~PO3tOd~$^t?|S5{GEK5YKlra>ygV93gtn{Gqt)W@)c%BEWa~uZDx`o`VOb%U>Oc*7lMdTm;F$EFp{31e2U&*fxs&o09u
zhiPRlGHXTH9Plhgc;(06{`K#E|Hps%Km7gw{15;5kH7oP4}bfYzcGL;wrxi-p1Gw8x(HxkXp_OG|4_dhCU0W7R{O(~R|j8xmUdeDfEQG#y
zI+NS96mBAdtSsJ{WLo&|e)D65;blK(U9F+C1_|`cFGBZKXk6YnAzeW!kKOKQAXh2$
zm@yOVJW5L-5x%>zs*>GX*_lSq%#0O#Z`4S~irLrk*>Yzht0#>+1E7XkjREt?^HFsR
zTt%mj(ryX3uHKga&)2#4%8llf85W8Jh4teeg0-i8}TF?ByuvWQW$y
z0AOicr>v6lw5;Y#BZj1z`Jg_-xH?cpcf~zQFjUPQ!K`Ch>mqYkRS$o2(`=|T
zcS~h$H1#n;1swmlQ`=OzPqyZ|$tk;4w@PxZTCv(-%n9Vq%q{b(BmV7J-|10hc`TB9
zCkVI(Lux9K-5>kk{fB@0`IldE@5kGtnmrdyXRWh#gfF>D4`*TTJEEuI^`PBw7K#}j
z;5QQ^V``!)^DuKaC)3O#yeZdTzX7mtbX?NyiJeUez|Oc>6{@>1Tyx1^0V|x9aPPj6
z+-ohsGWWW!EZ+D2^!Aon-|i0rJJZ9>9EIJ*lMPt9c^DZx>-+n!KYaeQ;`+GnzVAc|
z?+qzY%)E;G-jBysNj(o?u8Trx((CC)*6vcW3uY!RnBZyVR=!i^X7YzNenuIJx_;#a
zS1j{ISXRNxBdRQmDAl<
z{kqJ+QLZGRJ_|85n#ZfI%IXWuXl9|D)dQ8WEY>P;#br~lR|a}r4>xKw?BntI-djel
zYYwISm6L3uhV77<$8tn}(A{EPtgXzTxwEQGn!B!H&P+49hhLZV@hNxv^yy(9fBgCD
zFTeh(5wE}d?GNG1Q5<%Toav!k*Cd-vBJlJ_O8Of{C_q|!A=Z}kDx}*Gem?k9EV=hw
zw609(g}nhQ2KGGT@c`t2dL>3@I=*orECC=j(4zA*fF>PttG)E`W>P9^Y6GVQgNNW5
z9?H#1gNRQyV$bT76f5YYv}bH`kj1=Osb3IJ%sJN?&ZE@1=%LZgxgV|MG?Z-7xcBDV
zTnl+9j`v6BZme?Agm!qI-hjC`RBzX#U-VpR>CnjV7
zJ-Uq#Y78}ysTJ~R%r$e(QVw6bJ6IHeQXZdyCzYvlQl!+7d+*%add38-e-#P$7pTsno{I)celLecv+e
z(CtF35T2iQD0yQ&Qax`{Xu<9rq^J8iu-(T*foZlRRpc&RLnXq?&B{83nNw!ZMvKmp
zWWGwF{;ZoyZi!8`E6*Y6Q>1L#q5rQ_;T_5J((^SAF2%V2wF#51Skd}HP|^N2-KY^{fzS!R+mh`x_+
zHxp|LebWwRy3F}GrJxU{E_?2tQP#BD*r*uD2E1nOw!*^g@wPsFxgL+J)2>;(Rss3R
zAgtY~2wNK-riO+Xx}1bLqMK?s2e&JBA}-suiL`4o@MY#exJwW325d*rRd
z;<)*KJ8xD4Xw_+xB<&d|FKXPYkF}H454$6wibldJEv~~j9iMMUBBPhGshU{xe*Q9%
z66hOz&nvN>-uwgt*re$lHlB63{+mhX#Tj)rJ?F7Q^2G9?uzLISV~1E!%ouEd6QuNu
zSw)&_kd#?v^^N1+aBh5_(F+>%RmHxf$;`aUcJhzEzW?)|zA{51gqHUiB24^)oB0ZW
z|9`IjZOO7MNe;wRZSCV8k##|#(G7r24rXK?$p8NmzK;lnL!p3}Mm4Idvhot)XD`!v
zP_=bD89-NMWw@WS_qy0cO-(I4T?*9W?fUxV?ftDD88nlzpiHB`rw2ij46wz
zx4=ot;94Gyf(cqr<3-rN{=7lsM;3~&JR4)2fw?3z_34DH!1)Ip-*4a)02yV4-rZCM
zvq86>&;9ZJJBB0xkXQ$W^SNj=yuZOD?CB)R6*dw~QF!JlkYqK%Pxtsr0pL*20g|iI
z^Fdg1{Sw}f<6hd8BV`vP9b0JOWnIbtPIbLYo=+bzrwo-?Eg-?J3+1v*Ws!(;HXZ|T?Pv!%D|dVh0G
z$RoNE#a5OvH_tDNGT;+(uFd^o;Tn!cr(75PRd?aC3TBBW0L~E-gr2&veb!uyV?&ak
zBsTUfbSHlJD!H8|zn+GR+y)zg$<8#^Kc8Mz*Q&==*LrbhM>5p6NR>}B+~b%^#o7{A
zE=Uz0SmuaXw1Z%SCD>sgTQ^Z5nBmiz$koQxlY>qzHG_r}{o-U6SC2vRKHoTb1Yxdo7cMoZ4t=(Nb
zg^0`snO{jHYLZd!R93yF{`-L4!sxeVLo}VNxj;l(I%G*AVHzY~u|h)Jnbe(BPU!8H
ztVC3aT+qGUe*@s_WBsrH(|`Wk&p-X!zy0;U{>#7q?(6&SzrJP1c9`!x@f)TS@nqAP
z8l{$x=SA%(7nF~|F4WfO)V!(q-TQ#ep
zK$!^t=_L}Dh-+1*bN%w`w_m^C-JQAWj0j=8Ht1Ghoj?W7m|p3gXOe*#&bqaqdw=_O
ze|dlZ!w>Jw6i+W?1O#b9F;QKyY?AZqC9SohLWJZPUe_u8+Rg*+BY8dSd`$P29@mA;
z8Yy-G+U;?S?7a<;ncJHUcXfS&Z<`le#=ZC9H@FDOv({!!uj=mm>G5FLb2r_j?pJo0
zZ3|)8U|@xQKDYAuZ1sKXdOVJUZ#CWSFKd*#y^;6sVQ!Eu&RHHOq9UH%?isqu6~pa;
zb!SO|tuChgfcvTIx~l5FZ$dsHgc|BwRVDJi8&Z`>&)D0wE|&?Pt=+AsYOShrkJY}d
zm)X*G?(}?qHEi1L73~l@4x)@uRs3ufTr)zM}-amUUGpp44er=F9Vtg+aYRxvQ|3pJs!{b
zaCdt)f}eQr7Gz|N^Py!Q>OhOEbs=LQ=6bt;x?t)FBHCvebDJ4l*LCkF#I;u6x5!^U
z_FsScroJE7+gi1IKdwhvq78x};flvyRo}mT+Z$Z#`FwWox5s<;UF+KSz8?Dwi7eLr
z{oebwS9v@h&wXcu?4K|RC-D7wbBwd!&T8%ICU+dJqx5TqnRsihB~pu&~^>By8&H~$G*2*Co>hqVRrYmR-f3+v(-UP2jp50
z1N1dfK&lr8`d57db#z^i?6PqR^Dy>i)%AE7-4?X2MPOaE>h0b?e>}Guwbmbg_ulS(
z>b?>iDnlm8wo?28i_U7e6)xU`?*XHI{omjV-f2hLQA%a}00kYJ4%ZPBe5qTKXf
z$~y!gozWH|drK10j}CC(lc?I%%Q~NhcXiB9P->a;6=*zShK^
z9l|Ldp0HnIuSU5#%awRkh#FGjZH^sRV#>$`OqnVm2~H|GDf|oxuEA~sJd7#sF_L4G
z;l2la`u7&_1Y!Ji^1ZNot1`^ezeT(>X)dI5&WK8X+MU>hqht9fNK@_)*v
zk9AOP$@$&!akHC47%f)XI0*n%yrY@C9QEv(EZ|Ih@3glfD*=xh6|~ms-s`%qwWLlx
zlJIoHs=~U~T5^wI@8tWm^tZjX0YYA7>Su&uYQ$Id(u9yrABL){nbzSyUw`-#qqdwE
z$w`tgzlO7Je0R4YDw4dex*i^7$}jKlNMFmGJpkmMgw)*1S>FWCq`(Dw$o=aY4(6Qd
zG+!yHBZifWMW9htK9w~>EP|Hx(V>@3%r;=9ygetJQ!>gUSK|3FxiMFn^C<$X8+;PC
zI^aOwAl&_wh;k+~1_T6F6`H+l6*ei;mG*vCJ+5nAYps%qpMH7%w}1WdZ-4*U#>YYX
zEFxa~9=Rfj?%pHpOmg>zTiYupjly-UEm@7pVUgWj=3>lBp1YQAOo3pJYz7d}jpS8z
zEv~EH-q!p3;}P}7o~$+)!s}+9m7m}(6*IlW05RAZ#+QxhOS+ns9S}n=j_C_4(s&7-
z@uFkx2L=YC*^~Bo+gksH;RB5Uv9xU~Ka=1T`L4D|UdS;xS!2MDTAucKj`7RA^h{xY
z)qsNa_+Oq+cGDpO+#T=d&H0pi!#U2YGu}vo!Ml)41tJ4z`Wv%E4BX^^PLJz)e_W51@X%!S7rg%>A-K<
z>ei}mgaPwf3r$JQ|A6%xQ$rW?!J@r7?%&lz$^y6&h~TB|u$vg-3qNqu?#*xRcm>C$b_1Zpze0R28CLe&1+k6+beixWowFDG`J5FcPn6=-~BZ4hVfRQl%MQM8FUirKhzTN$Ce?0H~{!!m$5?D3RJx5|M?;tHJ;9Z%q&CPxJ0wK%z3
zRsu?Zp}})}I?pGLO`D^S$#l?gxa`WUF5+6@0i^Ib{>YXKrUy9*au+OwLU)(dGhiws
zx*+JPhtzlDW>nT>i=MMOnaZ#!<
z^Cu_gRgls-8HHMlYt@(cw;$fGHz&}{)|rOCDox^mtXjxIrSQgkixk`cGT<5}W}PE|
z5%Kd;$R3>3PM?ZpY&c!X^7@UMJ;bY-W(2#^F|lE1b?=(NA#FL0dEZ2*@v6zFy`E#0
zeb|#w9{C!d!~6UH{eSr{|Ic5){q65R{kK2=+yC;<|M7QU
z-qkI4yhT4br}J_P8okvhA-9EYGDIfOfd(O)9{JKUL6vq|%>Z$K;G*+QPO=Uc>
zX-`&Q?jF2F0GHeCy6=b^S1mf#@$-SPivFhLOWulfIZQ|kt>U%nx|TlnuTOP9?b&($
zFTINqk?ymsE|;Fxfr-+3M}?KBpMU-S_rL%8!1B?&p19Ye{&1d<+gKNNZW>?{;xfW6h6^@<}#i=(Ztc
zF%xy>Bts;(Jf5w_$RrynM!Cp8SLCg*trSt69uZl#_F9)q$B%k{?z^fIQ0Vs6x%a+U
zk9FO>l}H?f2e9>HEdf>7-Q8G4R(=0?%2nppZDl3^#rwX+Y313F?!?T!XIb&Q@2dK^
zpPh({_uX=PD9D-olr8~%>kKwTHkpEO-}~;ky1h41_q`#JU0nCR=dep;@3=FEB{{-F
zgN-BCp5U`xn;1It3Bomj+Uny}S#`06D2jrTzI=J_)|S3odvmKwwd(F&=`)TXfwid3
zjjdc!SzC61r$R30hAUgDcKthlbxB%@LCgxev4vvuqPrx`-Yf3=xz)ILE-i%n1IOVg
zQpsT9Vh5pXy6$~je$&IVkvS!L-kZd=*4}8{z+J_)2^$;gf1ncjW=XnNUA=D>
z@4I{NbzP21TJYf5VqL{mWrjO|#agRSqvPEeXU1J^|nAR
z%k`=6`({Jsw}ayj5|nt9bqWr(f>7SFJB!-hTMup@u{WP04%-M8jVg
z9+kNxRg$(g8Y|>sO5N(km4csJSIDAu`;%~-lDS*w)8^==&RSC1G(FZA<%VxXKd+>_
z@B6;4i)65Dr-GjCI&cmlvi5yyb|-RSN$Xlso8TJi5m3WFQWx4Du{y-vC&Nt3AcJ4@efTsXeS1yHqA88hpi7C3DwZ2;c4ey$5T`
zr%t53wI>vJ)cpx(iF=p`a|(4%IdQ^xCKBtGBS@Qqj7|*F5(X6l!Ixv)aVekDrD$rM
zdH{g)YA-f6;6bD(PXs1Mc6b)3U6U^6K;nFAk*w;y_kAjobI0#tZ)&>nO~!2RJe{J1JCQasBYa4!dg1BgyzD((o|p?C;^Nv@vwnIWbZKMQ7x390#-#Hr^qU-wSN5N{%`-|
z=Rg1T=jZbSnWr=GRM7k_qTD`|PEiuL9-e)#%cs}5uYWtIxVJRt}2ck=BTOqN_ODt_kkg)EYP
z_V|e+z#jh-W~&6)nwi2jB3!6o4hp=i!+MMnKo-vWy8{X8u$;##PU_(;OTwP_3<+@r
z4Ck#Foun$wijyK62;fVR+`Ty8vHb2z&kvaEk*E(19elIE5n5qW8!v%1Ao
ztSWZXntguFS07sOpBh@`B(+LB5glLaNpchYq4yX5%RjX>P>$*OvSogY$>BsX?MpbRNIdZDn40mcRM^+th2sy%Q
zG@L-9sZ0*`$?8D04wjB|CnB`tg#g_f_c|~3sQ%mTO;_})Tzs%ao8E~*W(Cq9Kr!w!
z|8L@I`$GfBXo$ZlMURtIl63!%zIVHIJ|M@UOJGhZ$08Vx>{s2}f=@o0F##H^a{R9s
zL}Z!HKw$6f>i58mxi7%&H>{!MLFnD;tt~BBwc>t0dtu-Hs!|Sc#ESjQaO(<^!G!E{iw=`NxSr6j!1LZ`6EBdxcbFx@aPDlE#VQw6Vt%qOI3;fTv*4vhrZ_KG
ziF0};a@EMhPk7F7(2L_THAO)VEOvzQY%ATYK&n!P$!3Jo<lQtY38-DC{PoVPx-!u;|CIOBrTC>5_hv;Bnh9iLB=L`N_Lfwi#Kfg4X(
zvENI`n6wGBmJ9WoXww%vo@q!*e!{A%D-(`lvwB)j)mk+DOb}En$^y|yh1QE_giq7R
z5@j-d!6`5Twt*g{K9NRe`TaH=md&RW^~9`?Rc~+C*SE*}s>Qw#t!$0VKr&AT$qFf#
zr}F~|C%oPS(AV_T#p~GWirk*mfBPM0{S<{3WBqO$)cL)yYcYyW|@+c0=0Rp;$#bDr=bPR26~UH|eH??NypZ-y1be
zOT`G{LuviNF;^M<+j04G%cqmGyF0>1zu8J4R67Qr?M5+eql?`4o`xk%<5^C6db!4X
zhiZM|%cSAx-Zo0wQ9rQtJ7;oQotk;}(n;}}{-kQ2EW@~L=PW*vn
z;L!n4V#<_l()#I$qFxQ!_RRB8v(Xsv%i(?xw@xEL=3AfA81bL~^vD0l|NLM6xBvJ5
z{x5(2*FXO5hc9pcVBU3(wR*yXJf&x2VYv`XQ`|0OD{pt6QOJ%eqxW6J&
zl;X;))?{o5fk{1*Pzj}Sw9YEWlNf;zCGF>P!(wYV4TCu|=-j>pMXk+_#94}e1U*Dn
zwm(1?GwkTZ2}9`7b}J#Lr4pD4)c`{dgmI<_w*>i7>f4rx`;c))piS5
ztM=X@San1C`2NJ=s>}EzEHMDw_p@s8#SYO4jQcp0#;Us3>fOX5@cwwqEh(+KPBu`h
zf>AIYnHZ;aWZ5LBKTD(MN9KrJ6;;M6R;})Rt!v(SqME%`^>07^^gsUgV->!Beg6-C
z`s4lat*X{_&0)9xp{iD-s=K9L*CmXSf4n5RZ;#s{>%MPcuUajvbr~8n#K~13&nJP<
z78J;XQmyO$`0!;YE|$mq7|_33?K&$m@bU(9?_tyM8tiQL065Ufuj7iwNG7Kr@
znOe1Kb?@?^!(y>K8#T0X8zXfstBo~*Xi2@WkJy+Z$-1A<^|%OK>)PGj+YnNb>+!hv
zz3M7M7i2&0Rf|=7W~Nqgi@*H(+`E7H-4DO~^7p^|^+z+}-yik*jXl$p4s4W1x*wY!Hw-md(`qk%U>4aFj
zo8(%{l}PVo!(0ery3YXJmK2+;8nRxU!i-!?pZbp>pv9uKj<4(LeQH;63fsXmks?#5
zIms$syroas&r
z?#$AblRV5J^0(M=%fV$Gti#1)VR~4d>t~VSu%>2A#ypRKf&gyaTE-v7)d$vK7MUS$
zy+V{wW0GA?QlES*!Cy}6oPhwIiHw84zz)U$cTvJtuOYhnV;t@6-R_E;ctf0>R<|-?
zksL%t-MlVns%HmM!wZ8>3L{T|H}5Su+TKm99@#k@YvyZQ>~&_HZ}3
zCi(>PhVds8K6baDc@oJPH1Ih9M9O
z7!(+(2+g5+CG1}2fN}66p+*bm>;wi_NbYrz@?mS@gm=KG7#xt;$5cS(YmDp4f%l;p
z4rP{o@SK>kIoo}%r|(mHkP6}X(6;-USm}%qx3BSbROD2AvI<8eJPhyXc64L+{`S59
z_VdS2zdU6dMkX3F4EGuU;j0jt22@?w_5OCfzgefh%#HW&C!5z?O#8_ig%ZN4rwz4v
zu-D_UY^Pjzvm(w5bM10Z7NTGJvJRO?&;xW%*sibp@LmoDFA75O^@ISi5)c
z&O$Qb0Y9O>7%VIo%-Q6)ea*kYlrB=!m;-t$I2zw!&p7QFX=%~X%LOuX+&6BxTx)aJ(
zK^neVrYLz=TH%8vZV=Qe8IK%C5epk=9szT*H8#9R@Z`hnjX32)UW+&+qvUD?6speI
zuv`%Tca06BWeqr)Ns-rbLsfxz?v1c2uz`!^c*}7>84LP!+})^S>?K{
za~Y&Sll3U>O`tHO@qW3;>C2d)rJG9A{cMw`!YJZ7%&p>SI@6FGYXL0R)kca8L=<3b
zHLLik^TovZ3+l(??fr4Rt@`q~=+k?=F?p@fZE^-{W*H+vtrMp68Xnvne&6ovT9Zml
zgZ6kDU;OP@+Q|;5^BBtj_$i80~rLaR^iKcT96W8m=9U^Kdv~^F+(>b8NjhF4KOf%Da4`ZGBGm
zk0{jqaK*Hpuo51G>Pvyd!GmQeHJ)+038aU_d4g1Y4pKFqEZ83p2X>-j<6m{|DH2Si
zs9&BpKfx57P&KS7kU(|_ee-o1Auij1;O1U!f<&?Iz4>|feHw|^G=tFH0$;R3(pgC>p+Q%Pnb_P9m^%EAPz
z-5b!}UDiBvBmVpPFaP4Z
zaJr*p_0_v(Tjp%gbrer!_r~9Q=ZZP_MbxhL-U7H*
z?>3a-Pk7!>M