diff --git a/createwebsite.sh b/createwebsite.sh index 646930d..f31db76 100755 --- a/createwebsite.sh +++ b/createwebsite.sh @@ -1,14 +1,4 @@ #!/bin/sh -cd $(dirname "$(readlink -f "$0")") -git clone https://centrifuge.hectabit.org/hectabit/burgernotes --depth=1 -cd burgernotes -mkdir -p ../website/static ../website/app ../website/error ../website/login ../website/logout ../website/privacy ../website/signup -cp -r static/* ../website/static -cp templates/app.html ../website/app/index.html -cp templates/error.html ../website/error/index.html -cp templates/login.html ../website/login/index.html -cp ../logout.html ../website/logout/index.html -cp templates/privacy.html ../website/privacy/index.html -cp templates/signup.html ../website/signup/index.html -cd .. -rm -rf burgernotes +git clone https://centrifuge.hectabit.org/hectabit/burgernotes-client-web.git --depth=1 +mv burgernotes-client-web/* website/ +rm -r burgernotes-client-web website/index.html website/README.md website/LICENSE diff --git a/website/app/index.html b/website/app/index.html index 27e5ac0..72111a2 100644 --- a/website/app/index.html +++ b/website/app/index.html @@ -8,6 +8,16 @@ + + diff --git a/website/error/index.html b/website/error/index.html index aa128e0..26bc137 100644 --- a/website/error/index.html +++ b/website/error/index.html @@ -7,6 +7,16 @@ + + diff --git a/website/login/index.html b/website/login/index.html index 8d75c11..d8c1b0a 100644 --- a/website/login/index.html +++ b/website/login/index.html @@ -8,18 +8,33 @@ - + + +

Image by perga (@pergagreen on discord)

+

Login

- + + -

+ +
+

Don't have an account? If so, Create one here!

+

Your homeserver is loading...

Change
Privacy & Terms
diff --git a/website/logout/index.html b/website/logout/index.html index 4d84463..f2c1f70 100644 --- a/website/logout/index.html +++ b/website/logout/index.html @@ -5,6 +5,16 @@ Burgernotes + + Logging out.. diff --git a/website/signup/index.html b/website/signup/index.html index 2d99ebd..e2dbceb 100644 --- a/website/signup/index.html +++ b/website/signup/index.html @@ -8,10 +8,21 @@ - + + +

Image by perga (@pergagreen on discord)

+

Signup

Signup for a Burgernotes account

@@ -19,10 +30,10 @@


+

Already have an account? If so, Login instead!

Please note that it's impossible to reset your password, do not forget it!

- By signing up, you agree to our Privacy & Terms.

-

Already have an account? If so, Login instead!


+

Your homeserver is loading...

Change
+ Privacy & Terms
- diff --git a/website/static/css/style.css b/website/static/css/style.css index 93eb6a0..1212323 100644 --- a/website/static/css/style.css +++ b/website/static/css/style.css @@ -1,6 +1,8 @@ @import url("../fonts/inter.css"); :root { + --contrast: #eee; + --contrast2: #fff; --invertdm: 0%; --bar: #f4f4f4; --editor: #ffffff; @@ -23,6 +25,8 @@ @media (prefers-color-scheme: dark) { :root { --invertdm: 100%; + --contrast: #2d2f21; + --contrast2: #2d2f21; --bar: #2d2f31; --editor: #202124; --text-color: #ffffff; @@ -52,7 +56,7 @@ } .newNote img { - filter: invert(100%) + filter: invert(100%); } #errorDiv p { @@ -64,7 +68,7 @@ } .burgerSession img { - filter: invert(100%) !important + filter: invert(100%) !important; } .links a { @@ -81,16 +85,16 @@ .inoutdiv input { color: white; - background-color: #202124; + background-color: var(--editor); } .flathubLogo { - filter: invert(100%) + filter: invert(100%); } .feature { background-color: rgba(0, 0, 0, 0) !important; - color: var(--text-color) + color: var(--text-color); } .mainDiv .yellow { border-color: #e9e98d !important; @@ -127,6 +131,14 @@ body { font-family: "Inter", sans-serif; } +.hiddenButton { + right: 0px; + position: fixed; + background-color: var(--editor); + color: var(--editor); + padding: 20px; +} + /* Web app */ .topBar { position: fixed; @@ -198,7 +210,6 @@ body { background-color: var(--bar); - border: solid; border-color: var(--border-color); border-width: 1px; @@ -210,19 +221,15 @@ body { width: calc(100% - 14px - 4px - 7px); color: var(--text-color); background-color: #ffffff; - height: 35px; line-height: 35px; margin: 7px; padding-left: 7px; - border: solid; border-color: var(--border-color); border-width: 1px; border-radius: 8px; - font-size: 15px; - text-decoration: none; } @@ -248,7 +255,6 @@ body { height: calc(100% - 50px - 30px - 1px); background-color: var(--bar); - border: solid; border-color: var(--border-color); border-width: 0px; @@ -270,27 +276,26 @@ body { margin-bottom: 0; background-color: var(--note-button); color: var(--unselected-note-button-text-color); - border: none; border-radius: 8px; border: solid; border-color: var(--border-color); border-width: 1px; - font-size: 15px; text-align: left; cursor: pointer; + white-space: nowrap; + overflow-x: hidden; } .notesBar .loadingStuff { border: none; - background: - linear-gradient(0.25turn, transparent, #fff, transparent), - linear-gradient(#eee, #eee), - radial-gradient(38px circle at 19px 19px, #eee 50%, transparent 51%), - linear-gradient(#eee, #eee); + linear-gradient(0.25turn, transparent, var(--contrast2), transparent), + linear-gradient(var(--contrast), var(--contrast)), + radial-gradient(38px circle at 19px 19px, #eee 50%, transparent 51%), + linear-gradient(var(--contrast), var(--contrast)); background-repeat: no-repeat; background-size: 315px 250px, 315px 180px, 100px 100px, 225px 30px; background-position: -315px 0, 0 0, 0px 190px, 50px 195px; @@ -317,7 +322,6 @@ body { background-color: rgba(0, 0, 0, 0); border: none; font-size: 16px; - margin-bottom: 5px; cursor: pointer; } @@ -344,7 +348,7 @@ body { } .noteBox:focus { - outline: none + outline: none; } .optionsCoverDiv { @@ -360,14 +364,14 @@ body { left: 50%; top: 50%; transform: translate(-50%, -50%); - width: 300px; position: fixed; background-color: var(--option-background); padding: 10px; color: var(--text-color); border-radius: 8px; - min-width: 338.5px; + min-width: 300px; z-index: 3; + } .optionsDiv button { @@ -384,6 +388,7 @@ body { background-color: var(--theme-color); border-radius: 8px; cursor: pointer; + } .optionsDiv .normalButton { @@ -398,7 +403,7 @@ body { .optionsDiv input { width: calc(100% - 12px); height: 25px; - background-color: ffffff; + background-color: #ffffff; padding-left: 5px; padding-right: 5px; margin-bottom: 7px; @@ -511,8 +516,11 @@ body { /* Sign up/log in div */ .inoutdiv { + border-radius: 8px; margin: 10%; - padding: 15px; + padding: 30px; + border: solid 1px var(--border-color); + background-color: var(--bar); } .inoutdiv input { @@ -526,6 +534,7 @@ body { border-color: var(--border-color); border-width: 1px; border-radius: 8px; + } .inoutdiv button { @@ -540,6 +549,7 @@ body { border-radius: 8px; font-size: 14px; + } .inoutdiv a { @@ -547,6 +557,23 @@ body { text-align: center; } +.background { + position: fixed; + z-index: -2; + top: 0; + width: 100%; + min-height: 100%; +} + +.credit { + position: fixed; + left: 5px; + color: white; + z-index: -1; + margin: 0; + bottom: 5px; +} + .hidden { display: none !important; } @@ -661,20 +688,24 @@ body { .links a { margin-left: 5px; text-decoration: none; - background-color: #f8f8f8; + background-color: var(--bar); color: var(--text-color); padding: 10px; + padding-top: 2.5px; + margin-bottom: 10px; border-radius: 10px; transition: background-color .2s; + display: inline-block; } .links a:hover { - background-color: #eaeaea; + background-color: var(--editor); } .links a img { transform: translateY(5px); padding-right: 10px; + filter: invert(var(--invertdm)); } .links a:hover { diff --git a/website/static/img/background.jpg b/website/static/img/background.jpg new file mode 100644 index 0000000..585ef41 Binary files /dev/null and b/website/static/img/background.jpg differ diff --git a/website/static/js/homeserver.js b/website/static/js/homeserver.js new file mode 100644 index 0000000..fb3bbb0 --- /dev/null +++ b/website/static/js/homeserver.js @@ -0,0 +1,49 @@ +let homeserverBox = document.getElementById("homeserverBox") +let statusBox = document.getElementById("statusBox") +let changeButton = document.getElementById("changeButton") + +function showElements(yesorno) { + if (!yesorno) { + homeserverBox.classList.add("hidden") + changeButton.classList.add("hidden") + } + else { + homeserverBox.classList.remove("hidden") + changeButton.classList.remove("hidden") + } +} + +changeButton.addEventListener("click", (event) => { + async function doStuff() { + let remote = homeserverBox.value + + if (remote == "") { + statusBox.innerText = "A homeserver is required!" + return + } + + showElements(false) + statusBox.innerText = "Connecting to homeserver..." + + fetch(remote + "/api/version") + .then((response) => response) + .then((response) => { + async function doStuff() { + if (response.status == 200) { + localStorage.setItem("homeserverURL", remote) + + window.location.href = document.referrer; + } + else if (response.status == 404) { + statusBox.innerText = "Not a valid homeserver!" + } + else { + statusBox.innerText = "Something went wrong!" + showElements(true) + } + } + doStuff() + }); + } + doStuff() +}); diff --git a/website/static/js/login.js b/website/static/js/login.js index e20da4c..a415070 100644 --- a/website/static/js/login.js +++ b/website/static/js/login.js @@ -9,6 +9,12 @@ if (localStorage.getItem("DONOTSHARE-password") !== null) { throw new Error(); } +let remote = localStorage.getItem("homeserverURL") +if (remote == null) { + localStorage.setItem("homeserverURL", "https://notes.hectabit.org") + remote = "https://notes.hectabit.org" +} + let usernameBox = document.getElementById("usernameBox") let passwordBox = document.getElementById("passwordBox") let statusBox = document.getElementById("statusBox") @@ -65,6 +71,10 @@ function showElements(yesorno) { } } +document.addEventListener('DOMContentLoaded', function() { + document.getElementById("homeserver").innerText = "Your homeserver is: " + remote + ". " +}); + signupButton.addEventListener("click", (event) => { if (passwordBox.classList.contains("hidden")) { if (usernameBox.value == "") { @@ -109,7 +119,7 @@ signupButton.addEventListener("click", (event) => { return key }; - fetch("https://notes.hectabit.org/api/login", { + fetch(remote + "/api/login", { method: "POST", body: JSON.stringify({ username: username, @@ -133,7 +143,7 @@ signupButton.addEventListener("click", (event) => { } else if (response.status == 401) { console.log("Trying oldhash") - fetch("https://notes.hectabit.org/api/login", { + fetch(remote + "/api/login", { method: "POST", body: JSON.stringify({ username: username, diff --git a/website/static/js/main.js b/website/static/js/main.js index 0cf630f..36634b5 100644 --- a/website/static/js/main.js +++ b/website/static/js/main.js @@ -13,6 +13,12 @@ if (localStorage.getItem("CACHE-username") !== null) { document.getElementById("usernameBox").innerText = localStorage.getItem("CACHE-username") } +let remote = localStorage.getItem("homeserverURL") +if (remote == null) { + localStorage.setItem("homeserverURL", "https://notes.hectabit.org") + remote = "https://notes.hectabit.org" +} + function formatBytes(a, b = 2) { if (!+a) return "0 Bytes"; const c = 0 > b ? 0 : b, d = Math.floor(Math.log(a) / Math.log(1000)); return `${parseFloat((a / Math.pow(1000, d)).toFixed(c))} ${["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"][d]}` } function truncateString(str, num) { @@ -196,8 +202,17 @@ textMinusBox.addEventListener("click", (event) => { }); +function truncateString(str, num) { + if (str.length > num) { + return str.slice(0, num) + ".."; + } else { + return str; + } +} + + function updateUserInfo() { - fetch("https://notes.hectabit.org/api/userinfo", { + fetch(remote + "/api/userinfo", { method: "POST", body: JSON.stringify({ secretKey: secretkey @@ -248,7 +263,7 @@ exitThing.addEventListener("click", (event) => { }); deleteMyAccountButton.addEventListener("click", (event) => { if (confirm("Are you REALLY sure that you want to delete your account? There's no going back!") == true) { - fetch("https://notes.hectabit.org/api/deleteaccount", { + fetch(remote + "/api/deleteaccount", { method: "POST", body: JSON.stringify({ secretKey: secretkey @@ -257,7 +272,6 @@ deleteMyAccountButton.addEventListener("click", (event) => { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { if (response.status == 200) { window.location.href = "../logout/index.html" @@ -271,7 +285,7 @@ sessionManagerButton.addEventListener("click", (event) => { optionsDiv.classList.add("hidden") sessionManagerDiv.classList.remove("hidden") - fetch("https://notes.hectabit.org/api/sessions/list", { + fetch(remote + "/api/sessions/list", { method: "POST", body: JSON.stringify({ secretKey: secretkey @@ -280,7 +294,6 @@ sessionManagerButton.addEventListener("click", (event) => { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { async function doStuff() { let responseData = await response.json() @@ -292,9 +305,9 @@ sessionManagerButton.addEventListener("click", (event) => { let sessionRemoveButton = document.createElement("button") sessionText.classList.add("w300") if (responseData[i]["thisSession"] == true) { - sessionText.innerText = "(current) " + truncateString(responseData[i]["device"], 18) + sessionText.innerText = "(current) " + responseData[i]["device"] } else { - sessionText.innerText = truncateString(responseData[i]["device"], 27) + sessionText.innerText = responseData[i]["device"] } sessionText.title = responseData[i]["device"] sessionRemoveButton.innerText = "x" @@ -311,7 +324,7 @@ sessionManagerButton.addEventListener("click", (event) => { } sessionRemoveButton.addEventListener("click", (event) => { - fetch("https://notes.hectabit.org/api/sessions/remove", { + fetch(remote + "/api/sessions/remove", { method: "POST", body: JSON.stringify({ secretKey: secretkey, @@ -321,7 +334,6 @@ sessionManagerButton.addEventListener("click", (event) => { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { if (responseData[i]["thisSession"] == true) { window.location.replace("../logout/index.html") @@ -362,7 +374,7 @@ function selectNote(nameithink) { let thingArray = Array.from(document.querySelectorAll(".noteButton")).find(el => el.id == nameithink); thingArray.classList.add("selected") - fetch("https://notes.hectabit.org/api/readnote", { + fetch(remote + "/api/readnote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, @@ -378,7 +390,6 @@ function selectNote(nameithink) { noteBox.placeholder = "" displayError("Something went wrong... Please try again later!") }) - .then((response) => response) .then((response) => { selectedNote = nameithink noteBox.readOnly = false @@ -397,21 +408,28 @@ function selectNote(nameithink) { updateWordCount() clearTimeout(timer); timer = setTimeout(() => { + let encryptedTitle = "New note" + if (noteBox.value.substring(0, noteBox.value.indexOf("\n")) != "") { + let firstTitle = noteBox.value.substring(0, noteBox.value.indexOf("\n")); + + document.getElementById(nameithink).innerText = firstTitle + encryptedTitle = CryptoJS.AES.encrypt(firstTitle, password).toString(); + } let encryptedText = CryptoJS.AES.encrypt(noteBox.value, password).toString(); if (selectedNote == nameithink) { - fetch("https://notes.hectabit.org/api/editnote", { + fetch(remote + "/api/editnote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, noteId: nameithink, content: encryptedText, + title: encryptedTitle }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { if (response.status == 418) { displayError("You've ran out of storage... Changes will not be saved until you free up storage!") @@ -429,7 +447,7 @@ function selectNote(nameithink) { } function updateNotes() { - fetch("https://notes.hectabit.org/api/listnotes", { + fetch(remote + "/api/listnotes", { method: "POST", body: JSON.stringify({ secretKey: secretkey @@ -438,7 +456,6 @@ function updateNotes() { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { async function doStuff() { document.querySelectorAll(".noteButton").forEach((el) => el.remove()); @@ -459,11 +476,11 @@ function updateNotes() { let originalTitle = bytes.toString(CryptoJS.enc.Utf8); noteButton.id = responseData[i]["id"] - noteButton.innerText = originalTitle + noteButton.innerText = truncateString(originalTitle, 15) noteButton.addEventListener("click", (event) => { if (event.ctrlKey) { - fetch("https://notes.hectabit.org/api/removenote", { + fetch(remote + "/api/removenote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, @@ -473,7 +490,6 @@ function updateNotes() { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { updateNotes() }) @@ -494,38 +510,29 @@ function updateNotes() { updateNotes() newNote.addEventListener("click", (event) => { - let noteName = displayPrompt("Note name?", "E.G Shopping list", burgerFunction) - function burgerFunction(noteName) { - if (noteName != null) { - if (noteName.length > 21) { - displayError("Invalid note name: Too long (max 21 characters)"); - return; - } - - let encryptedName = CryptoJS.AES.encrypt(noteName, password).toString(); - fetch("https://notes.hectabit.org/api/newnote", { - method: "POST", - body: JSON.stringify({ - secretKey: secretkey, - noteName: encryptedName, - }), - headers: { - "Content-Type": "application/json; charset=UTF-8" - } - }) - .catch((error) => { - displayError("Failed to create new note, please try again later...") - }) - .then((response) => { - if (response.status !== 200) { - updateNotes() - displayError("Failed to create new note (HTTP error code " + response.status + ")") - } else { - updateNotes() - } - }); + let noteName = "New note" + let encryptedName = CryptoJS.AES.encrypt(noteName, password).toString(); + fetch(remote + "/api/newnote", { + method: "POST", + body: JSON.stringify({ + secretKey: secretkey, + noteName: encryptedName, + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" } - } + }) + .catch((error) => { + displayError("Failed to create new note, please try again later...") + }) + .then((response) => { + if (response.status !== 200) { + updateNotes() + displayError("Failed to create new note (HTTP error code " + response.status + ")") + } else { + updateNotes() + } + }); }); function downloadObjectAsJson(exportObj, exportName) { var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj)); @@ -539,7 +546,7 @@ function downloadObjectAsJson(exportObj, exportName) { function exportNotes() { let noteExport = [] - fetch("https://notes.hectabit.org/api/exportnotes", { + fetch(remote + "/api/exportnotes", { method: "POST", body: JSON.stringify({ secretKey: secretkey @@ -548,7 +555,6 @@ function exportNotes() { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { async function doStuff() { let responseData = await response.json() @@ -588,6 +594,16 @@ function isFirstTimeVisitor() { } } +function firstNewVersion() { + if (document.cookie.indexOf("version=1.2") !== -1) { + return false; + } else { + var expirationDate = new Date(); + expirationDate.setFullYear(expirationDate.getFullYear() + 1); + document.cookie = "version=1.2; expires=" + expirationDate.toUTCString() + "; path=/; SameSite=strict"; + return true; + } +} exportNotesButton.addEventListener("click", (event) => { exportNotesButton.innerText = "Downloading..." @@ -598,7 +614,7 @@ removeBox.addEventListener("click", (event) => { if (selectedNote == 0) { displayError("You need to select a note first!") } else { - fetch("https://notes.hectabit.org/api/removenote", { + fetch(remote + "/api/removenote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, @@ -608,7 +624,6 @@ removeBox.addEventListener("click", (event) => { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => response) .then((response) => { updateNotes() }) @@ -621,3 +636,7 @@ removeBox.addEventListener("click", (event) => { if (isFirstTimeVisitor() && /Android|iPhone|iPod/i.test(navigator.userAgent)) { displayError("To use Burgernotes:\n Swipe Right on a note to open it\n Swipe left in the text boxes to return to notes\n Click on a note to highlight it") } + +if (firstNewVersion()) { + displayError("What's new in Burgernotes 1.2?\n\nNote titles are now the first line of a note \(will not break compatibility with older notes\)\nIntroduced improved login screen\nNote titles now scroll correctly") +} diff --git a/website/static/js/signup.js b/website/static/js/signup.js index 9452651..e2a0221 100644 --- a/website/static/js/signup.js +++ b/website/static/js/signup.js @@ -9,6 +9,12 @@ if (localStorage.getItem("DONOTSHARE-password") !== null) { throw new Error(); } +let remote = localStorage.getItem("homeserverURL") +if (remote == null) { + localStorage.setItem("homeserverURL", "https://notes.hectabit.org") + remote = "https://notes.hectabit.org" +} + let usernameBox = document.getElementById("usernameBox") let passwordBox = document.getElementById("passwordBox") let statusBox = document.getElementById("statusBox") @@ -27,6 +33,10 @@ function showElements(yesorno) { } } +document.addEventListener('DOMContentLoaded', function() { + document.getElementById("homeserver").innerText = "Your homeserver is: " + remote + ". " +}); + signupButton.addEventListener("click", (event) => { async function doStuff() { let username = usernameBox.value @@ -61,7 +71,7 @@ signupButton.addEventListener("click", (event) => { }; - fetch("https://notes.hectabit.org/api/signup", { + fetch(remote + "/api/signup", { method: "POST", body: JSON.stringify({ username: username, diff --git a/website/static/svg/favicon.svg b/website/static/svg/favicon.svg new file mode 100644 index 0000000..6e7f325 --- /dev/null +++ b/website/static/svg/favicon.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/svg/grid.svg b/website/static/svg/grid.svg new file mode 100644 index 0000000..fc91a30 --- /dev/null +++ b/website/static/svg/grid.svg @@ -0,0 +1,3535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +