GreasyFork: download script button

If you have a script manager and you want to download some script without installing it, this script will help

< Feedback on GreasyFork: download script button

Review: Good - script works

§
Posted: 2023-12-13

Hello,

With the latest version 2.1.4 of your script, I always get this error message to download a userscript or a userstyle like the screenshots show :-(

image

image

image

Thank you for your answer.

Google Chrome Browser with Tampermonkey and Violentmonkey

KonfAuthor
§
Posted: 2023-12-13

I'll check it out. Is there are any messenger I can reach you to debug the problem faster?

KonfAuthor
§
Posted: 2023-12-13

Nevermind. It seems like the problem is general, I also have it. It's because of changes on the website... I'll see what I can do

KonfAuthor
§
Posted: 2023-12-13

Uhmm... I did nothing and it works for me again. I'm confused

§
Posted: 2023-12-13

Incredible, I just tested again and it's always the same on my side 😲

KonfAuthor
§
Posted: 2023-12-13

Do you use discord or something where I can text you? As long as I can't reproduce the problem on my side, I can't fix it

§
Posted: 2023-12-13

Yes by e-mail, because I don't have a social network.

KonfAuthor
§
Posted: 2023-12-13

Ok let's stick with this place for now. I need some screenshots or recording that is showing what is the reason of the error. I need something like this: https://streamable.com/fj29le

It's my video where I'm just offline trying to download something. It would be great if you can make something similar and upload it using the same video share service or maybe some better one because this is kinda shit, loses video quality

KonfAuthor
§
Posted: 2023-12-13

Show the exact things I'm showing on the video

KonfAuthor
§
Posted: 2023-12-14

I've made some changes, but I have no idea if they would have any impact or not. Test this:

// ==UserScript==
// @name          GreasyFork: download script button
// @description   If you have a script manager and you want to download some script without installing it, this script will help
// @author        Konf
// @version       2.1.5a
// @namespace     https://greasyfork.org/users/424058
// @icon          https://greasyfork.org/vite/assets/blacklogo96-e0c2c761.png
// @match         https://greasyfork.org/*/scripts/*
// @match         https://sleazyfork.org/*/scripts/*
// @connect       update.greasyfork.org
// @compatible    Chrome
// @compatible    Opera
// @compatible    Firefox
// @run-at        document-end
// @grant         GM_addStyle
// @grant         GM_xmlhttpRequest
// @noframes
// ==/UserScript==

/* jshint esversion: 8 */

(function() {
  'use strict';

  const i18n = {
    download: 'download',
    downloadWithoutInstalling: 'downloadWithoutInstalling',
    failedToDownload: 'failedToDownload',
  };

  const translate = (function() {
    const userLang = location.pathname.split('/')[1];
    const strings = {
      'en': {
        [i18n.download]: 'Download ⇩',
        [i18n.downloadWithoutInstalling]: 'Download without installing',
        [i18n.failedToDownload]:
          'Failed to download the script. There is might be more info in the browser console',
      },
      'ru': {
        [i18n.download]: 'Скачать ⇩',
        [i18n.downloadWithoutInstalling]: 'Скачать не устанавливая',
        [i18n.failedToDownload]:
          'Не удалось скачать скрипт. Больше информации может быть в консоли браузера',
      },
      'zh-CN': {
        [i18n.download]: '下载 ⇩',
        [i18n.downloadWithoutInstalling]: '下载此脚本',
        [i18n.failedToDownload]: '无法下载此脚本',
      },
    };

    return id => (strings[userLang] || strings.en)[id] || strings.en[id];
  }());

  const installBtns = document.querySelectorAll('a.install-link');
  const installArea = document.querySelector('div#install-area');
  const installHelpLinks = document.querySelectorAll('a.install-help-link');
  const suggestion = document.querySelector('div#script-feedback-suggestion');
  const libraryRequire = document.querySelector('div#script-content > p > code');

  // if a script/style is detected
  if (
    installArea &&
    (installBtns.length > 0) &&
    (installBtns.length === installHelpLinks.length)
  ) {
    for (let i = 0; i < installBtns.length; i++) {
      mountScriptDownloadButton(installBtns[i], installArea, installHelpLinks[i]);
    }
  }
  // or maybe a library
  else if (suggestion && libraryRequire) {
    mountLibraryDownloadButton(suggestion, libraryRequire);
  }

  function mountScriptDownloadButton(
    installBtn,
    installArea,
    installHelpLink,
  ) {
    if (!installBtn.href) throw new Error('script href is not found');

    // https://img.icons8.com/pastel-glyph/64/ffffff/download.png
    // array to fold the string in a code editor
    const downloadIconBase64 = [
      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaX',
      'HeAAAABmJLR0QA/wD/AP+gvaeTAAABgUlEQVR4nO3ZTU6DUAAE4HnEk+jWG3TrHV',
      'wY3XoEt23cGleamtRtTbyPS3sCV0bXjptHRAIEsM/hZ76kCZRHGaZAGwDMzMzMbJ',
      '6CasMkMwBncXYbQvhSZZEgecEf56ocmWrDAA4L00eqEMoCBsEFqAOouQB1ADUXoA',
      '6g5gLUAdRcgDqAmgtQB1BzAeoAakkLIHlN8pPkDcnWd59IBpK3cd1VyoxJkfwo3P',
      'V5KJZAcllYtiy8H+LY3HvKjKlPgU1h+hLAuulIiMvWcWzVZ4xL/Dbv+Nsjyax8BM',
      'Sx96Wxm3jzdLwaSliVCpjezucqzmuSfKuZJkvXi0moORKqTOebL2tRwnR3PtdQwv',
      'R3PldRgmznlc8GA4DTOPscQqAqy6x1+X8+6Ke5yfNxIE9z6/TN1+XCM4inuQ165Z',
      'vHz04DF6AOoOYC1AHUXIA6gNpBz/UWJK/2muTvFn1W6lvASXyNXpdTYJcsxf69th',
      '3Y5QjYAiCA485x/tcLgCd1CDMzMzMbum8+xtkWw6QCvwAAAABJRU5ErkJggg==',
    ].join('');

    GM_addStyle([`
      .GF-DSB__script-download-button {
        position: relative;
        padding: 8px 22px;
        cursor: pointer;
        border: none;
        background: #0F750F;
        transition: box-shadow 0.2s;
      }

      .GF-DSB__script-download-button:hover,
      .GF-DSB__script-download-button:focus {
        box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
      }


      .GF-DSB__script-download-icon {
        position: absolute;
      }

      .GF-DSB__script-download-icon--download {
        width: 30px;
        height: 30px;
        top: 4px;
        left: 7px;
      }

      .GF-DSB__script-download-icon--loading,
      .GF-DSB__script-download-icon--loading:after {
        border-radius: 50%;
        width: 16px;
        height: 16px;
      }

      .GF-DSB__script-download-icon--loading {
        top: 8px;
        left: 11px;
        border-top: 3px solid rgba(255, 255, 255, 0.2);
        border-right: 3px solid rgba(255, 255, 255, 0.2);
        border-bottom: 3px solid rgba(255, 255, 255, 0.2);
        border-left: 3px solid #ffffff;
        transform: translateZ(0);
        object-position: -99999px;
        animation: GF-DSB__script-download-loading-icon 1.1s infinite linear;
      }

      @keyframes GF-DSB__script-download-loading-icon {
        0% {
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(360deg);
        }
      }
    `][0]);

    const b = document.createElement('a');
    const bIcon = document.createElement('img');

    b.href = '#';
    b.title = translate(i18n.downloadWithoutInstalling);
    b.draggable = false;
    b.className = 'GF-DSB__script-download-button';

    bIcon.src = downloadIconBase64;
    bIcon.draggable = false;
    bIcon.className =
      'GF-DSB__script-download-icon GF-DSB__script-download-icon--download';

    installHelpLink.style.position = 'relative'; // shadows bugfix

    b.appendChild(bIcon);
    installArea.insertBefore(b, installHelpLink);

    // against doubleclicks
    let isFetchingAllowed = true;

    async function clicksHandler(ev) {
      ev.preventDefault();

      setTimeout(() => b === document.activeElement && b.blur(), 250);

      if (isFetchingAllowed === false) return;

      isFetchingAllowed = false;
      bIcon.className =
        'GF-DSB__script-download-icon GF-DSB__script-download-icon--loading';

      try {
        await downloadScript({
          fileExt: `.user.${installBtn.dataset.installFormat || 'txt'}`,
          href: installBtn.href,
          name: installBtn.dataset.scriptName,
        });
      } catch (e) {
        console.error(e);
        alert(`${translate(i18n.failedToDownload)}: \n${e}`);
      } finally {
        setTimeout(() => {
          isFetchingAllowed = true;
          bIcon.className =
            'GF-DSB__script-download-icon GF-DSB__script-download-icon--download';
        }, 300);
      }
    }

    b.addEventListener('click', clicksHandler);
    b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  }

  function mountLibraryDownloadButton(suggestion, libraryRequire) {
    const [
      libraryHref,
      libraryName,
    ] = libraryRequire.innerText.match(
      /\/\/ @require (https:\/\/.+\/scripts\/\d+\/\d+\/(.*)\.js)/
    ).slice(1);

    // this probably is completely useless but whatever
    if (!libraryHref) throw new Error('library href is not found');

    GM_addStyle([`
      .GF-DSB__library-download-button {
        transition: box-shadow 0.2s;
      }

      .GF-DSB__library-download-button--loading {
        animation: GF-DSB__loading-text 1s infinite linear;
      }

      @keyframes GF-DSB__loading-text {
        50% {
          opacity: 0.4;
        }
      }
    `][0]);

    const b = document.createElement('a');

    b.href = '#';
    b.draggable = false;
    b.innerText = translate(i18n.download);
    b.className = 'GF-DSB__library-download-button';

    suggestion.appendChild(b);

    // against doubleclicks
    let isFetchingAllowed = true;

    async function clicksHandler(ev) {
      ev.preventDefault();

      setTimeout(() => b === document.activeElement && b.blur(), 250);

      if (isFetchingAllowed === false) return;

      isFetchingAllowed = false;
      b.className =
        'GF-DSB__library-download-button GF-DSB__library-download-button--loading';

      try {
        await downloadScript({
          fileExt: '.js',
          href: libraryHref,
          name: libraryName,
        });
      } catch (e) {
        console.error(e);
        alert(`${translate(i18n.failedToDownload)}: \n${e}`);
      } finally {
        setTimeout(() => {
          isFetchingAllowed = true;
          b.className = 'GF-DSB__library-download-button';
        }, 300);
      }
    }

    b.addEventListener('click', clicksHandler);
    b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  }

  // utils --------------------------------------------------------------------

  async function downloadScript({
    fileExt = '.txt',
    href,
    name = Date.now(),
  } = {}) {
    if (!href) throw new Error('href is missing');

    const result = await GM.xmlHttpRequest({ url: href });

    if (result.status !== 200) {
      throw new Error(`Bad response: ${result.status}`);
    }

    const blob = new Blob([result.response], { type: 'text/plain' });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');

    a.href = url;
    a.download = `${name}${fileExt}`;
    document.body.appendChild(a); // is needed due to firefox bug
    a.click();
    a.remove();

    window.URL.revokeObjectURL(url);
  }
}());
§
Posted: 2023-12-19

Hello,

Sorry for my late reply.

Unfortunately, there are the problems in this version 2.1.5a on my side :-(

image

image

KonfAuthor
§
Posted: 2023-12-19

Was that Tampermonkey? If it was not, could you try it? Also what is the Chrome version?

§
Posted: 2023-12-19

This Tampermonkey version 5 for Google Chrome -> https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo
As I'm running Windows 8.1, Google Chrome forced me to stay on their version 109 :-(

KonfAuthor
§
Posted: 2023-12-19
Edited: 2023-12-19

version 109

Ok, I'll check this out a bit later. I'll make one more script version to test

§
Posted: 2023-12-19

However the versions 1.2.3 and 2.0.0 work always very well :-)

KonfAuthor
§
Posted: 2023-12-19

Got it. I'll get this 109 and would fix the script latest version for it

KonfAuthor
§
Posted: 2023-12-19

This is so weird that I don't have any of these errors on 109 as well. I guess I'll just try something in blind

KonfAuthor
§
Posted: 2023-12-19

Can you tell the exact version? Somewhere in 3-dot menu then About or something like that

chrome://settings/help I have 109.0.5414.0 for example

§
Posted: 2023-12-19

The Version 109.0.5414.168 (Build officiel) (64 bits)

KonfAuthor
§
Posted: 2023-12-19
Edited: 2023-12-19

Try this. If this works, I'll release it then, and you would need to reinstall it from the script page in order to be able to receive a next updates automatically

// ==UserScript==
// @name          GreasyFork: download script button
// @description   If you have a script manager and you want to download some script without installing it, this script will help
// @author        Konf
// @version       2.1.5b
// @namespace     https://greasyfork.org/users/424058
// @icon          https://greasyfork.org/vite/assets/blacklogo96-e0c2c761.png
// @match         https://greasyfork.org/*/scripts/*
// @match         https://sleazyfork.org/*/scripts/*
// @compatible    Chrome
// @compatible    Opera
// @compatible    Firefox
// @run-at        document-end
// @grant         GM_addStyle
// @noframes
// ==/UserScript==

/* jshint esversion: 8 */

(function() {
  'use strict';

  const i18n = {
    download: 'download',
    downloadWithoutInstalling: 'downloadWithoutInstalling',
    failedToDownload: 'failedToDownload',
  };

  const translate = (function() {
    const userLang = location.pathname.split('/')[1];
    const strings = {
      'en': {
        [i18n.download]: 'Download ⇩',
        [i18n.downloadWithoutInstalling]: 'Download without installing',
        [i18n.failedToDownload]:
          'Failed to download the script. There is might be more info in the browser console',
      },
      'ru': {
        [i18n.download]: 'Скачать ⇩',
        [i18n.downloadWithoutInstalling]: 'Скачать не устанавливая',
        [i18n.failedToDownload]:
          'Не удалось скачать скрипт. Больше информации может быть в консоли браузера',
      },
      'zh-CN': {
        [i18n.download]: '下载 ⇩',
        [i18n.downloadWithoutInstalling]: '下载此脚本',
        [i18n.failedToDownload]: '无法下载此脚本',
      },
    };

    return id => (strings[userLang] || strings.en)[id] || strings.en[id];
  }());

  const installBtns = document.querySelectorAll('a.install-link');
  const installArea = document.querySelector('div#install-area');
  const installHelpLinks = document.querySelectorAll('a.install-help-link');
  const suggestion = document.querySelector('div#script-feedback-suggestion');
  const libraryRequire = document.querySelector('div#script-content > p > code');

  // if a script/style is detected
  if (
    installArea &&
    (installBtns.length > 0) &&
    (installBtns.length === installHelpLinks.length)
  ) {
    for (let i = 0; i < installBtns.length; i++) {
      mountScriptDownloadButton(installBtns[i], installArea, installHelpLinks[i]);
    }
  }
  // or maybe a library
  else if (suggestion && libraryRequire) {
    mountLibraryDownloadButton(suggestion, libraryRequire);
  }

  function mountScriptDownloadButton(
    installBtn,
    installArea,
    installHelpLink,
  ) {
    if (!installBtn.href) throw new Error('script href is not found');

    // https://img.icons8.com/pastel-glyph/64/ffffff/download.png
    // array to fold the string in a code editor
    const downloadIconBase64 = [
      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaX',
      'HeAAAABmJLR0QA/wD/AP+gvaeTAAABgUlEQVR4nO3ZTU6DUAAE4HnEk+jWG3TrHV',
      'wY3XoEt23cGleamtRtTbyPS3sCV0bXjptHRAIEsM/hZ76kCZRHGaZAGwDMzMzMbJ',
      '6CasMkMwBncXYbQvhSZZEgecEf56ocmWrDAA4L00eqEMoCBsEFqAOouQB1ADUXoA',
      '6g5gLUAdRcgDqAmgtQB1BzAeoAakkLIHlN8pPkDcnWd59IBpK3cd1VyoxJkfwo3P',
      'V5KJZAcllYtiy8H+LY3HvKjKlPgU1h+hLAuulIiMvWcWzVZ4xL/Dbv+Nsjyax8BM',
      'Sx96Wxm3jzdLwaSliVCpjezucqzmuSfKuZJkvXi0moORKqTOebL2tRwnR3PtdQwv',
      'R3PldRgmznlc8GA4DTOPscQqAqy6x1+X8+6Ke5yfNxIE9z6/TN1+XCM4inuQ165Z',
      'vHz04DF6AOoOYC1AHUXIA6gNpBz/UWJK/2muTvFn1W6lvASXyNXpdTYJcsxf69th',
      '3Y5QjYAiCA485x/tcLgCd1CDMzMzMbum8+xtkWw6QCvwAAAABJRU5ErkJggg==',
    ].join('');

    GM_addStyle([`
      .GF-DSB__script-download-button {
        position: relative;
        padding: 8px 22px;
        cursor: pointer;
        border: none;
        background: #0F750F;
        transition: box-shadow 0.2s;
      }

      .GF-DSB__script-download-button:hover,
      .GF-DSB__script-download-button:focus {
        box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
      }


      .GF-DSB__script-download-icon {
        position: absolute;
      }

      .GF-DSB__script-download-icon--download {
        width: 30px;
        height: 30px;
        top: 4px;
        left: 7px;
      }

      .GF-DSB__script-download-icon--loading,
      .GF-DSB__script-download-icon--loading:after {
        border-radius: 50%;
        width: 16px;
        height: 16px;
      }

      .GF-DSB__script-download-icon--loading {
        top: 8px;
        left: 11px;
        border-top: 3px solid rgba(255, 255, 255, 0.2);
        border-right: 3px solid rgba(255, 255, 255, 0.2);
        border-bottom: 3px solid rgba(255, 255, 255, 0.2);
        border-left: 3px solid #ffffff;
        transform: translateZ(0);
        object-position: -99999px;
        animation: GF-DSB__script-download-loading-icon 1.1s infinite linear;
      }

      @keyframes GF-DSB__script-download-loading-icon {
        0% {
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(360deg);
        }
      }
    `][0]);

    const b = document.createElement('a');
    const bIcon = document.createElement('img');

    b.href = '#';
    b.title = translate(i18n.downloadWithoutInstalling);
    b.draggable = false;
    b.className = 'GF-DSB__script-download-button';

    bIcon.src = downloadIconBase64;
    bIcon.draggable = false;
    bIcon.className =
      'GF-DSB__script-download-icon GF-DSB__script-download-icon--download';

    installHelpLink.style.position = 'relative'; // shadows bugfix

    b.appendChild(bIcon);
    installArea.insertBefore(b, installHelpLink);

    // against doubleclicks
    let isFetchingAllowed = true;

    async function clicksHandler(ev) {
      ev.preventDefault();

      setTimeout(() => b === document.activeElement && b.blur(), 250);

      if (isFetchingAllowed === false) return;

      isFetchingAllowed = false;
      bIcon.className =
        'GF-DSB__script-download-icon GF-DSB__script-download-icon--loading';

      try {
        await downloadScript({
          fileExt: `.user.${installBtn.dataset.installFormat || 'txt'}`,
          href: installBtn.href,
          name: installBtn.dataset.scriptName,
        });
      } catch (e) {
        console.error(e);
        alert(`${translate(i18n.failedToDownload)}: \n${e}`);
      } finally {
        setTimeout(() => {
          isFetchingAllowed = true;
          bIcon.className =
            'GF-DSB__script-download-icon GF-DSB__script-download-icon--download';
        }, 300);
      }
    }

    b.addEventListener('click', clicksHandler);
    b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  }

  function mountLibraryDownloadButton(suggestion, libraryRequire) {
    const [
      libraryHref,
      libraryName,
    ] = libraryRequire.innerText.match(
      /\/\/ @require (https:\/\/.+\/scripts\/\d+\/\d+\/(.*)\.js)/
    ).slice(1);

    // this probably is completely useless but whatever
    if (!libraryHref) throw new Error('library href is not found');

    GM_addStyle([`
      .GF-DSB__library-download-button {
        transition: box-shadow 0.2s;
      }

      .GF-DSB__library-download-button--loading {
        animation: GF-DSB__loading-text 1s infinite linear;
      }

      @keyframes GF-DSB__loading-text {
        50% {
          opacity: 0.4;
        }
      }
    `][0]);

    const b = document.createElement('a');

    b.href = '#';
    b.draggable = false;
    b.innerText = translate(i18n.download);
    b.className = 'GF-DSB__library-download-button';

    suggestion.appendChild(b);

    // against doubleclicks
    let isFetchingAllowed = true;

    async function clicksHandler(ev) {
      ev.preventDefault();

      setTimeout(() => b === document.activeElement && b.blur(), 250);

      if (isFetchingAllowed === false) return;

      isFetchingAllowed = false;
      b.className =
        'GF-DSB__library-download-button GF-DSB__library-download-button--loading';

      try {
        await downloadScript({
          fileExt: '.js',
          href: libraryHref,
          name: libraryName,
        });
      } catch (e) {
        console.error(e);
        alert(`${translate(i18n.failedToDownload)}: \n${e}`);
      } finally {
        setTimeout(() => {
          isFetchingAllowed = true;
          b.className = 'GF-DSB__library-download-button';
        }, 300);
      }
    }

    b.addEventListener('click', clicksHandler);
    b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  }

  // utils --------------------------------------------------------------------

  // Is needed because you can't fetch a new format script link
  // due to different domain cors restriction...
  function convertScriptHrefToAnOldFormat(href) {
    const regex = /https:\/\/update\.(\w+\.org)\/scripts\/(\d+)\/(\d+\/)?(.+)/;
    const match = href.match(regex);

    if (!match) throw new Error("can't convert href to an old format");

    const domain = match[1];
    const scriptId = match[2];
    const version = match[3] ? `?version=${match[3]}` : '';
    const scriptName = match[4];

    return `https://${domain}/scripts/${scriptId}/code/${scriptName}${version}`;
  }

  async function downloadScript({
    fileExt = '.txt',
    href,
    name = Date.now(),
  } = {}) {
    if (!href) throw new Error('Script href is missing');

    const fetchErrors = [];
    let url;

    // Consider first attempt as a main one. Second one is
    // needed just for some unknown edge case scenarios. See link:
    // https://greasyfork.org/scripts/420872/discussions/216921
    for (const scriptHref of [
      convertScriptHrefToAnOldFormat(href),
      href,
    ]) {
      try {
        const response = await fetch(scriptHref);

        if (response.status !== 200) {
          throw new Error(`Bad response: ${response.status}`);
        }

        url = window.URL.createObjectURL(await response.blob());
      } catch (e) {
        fetchErrors.push(e);
      }
    }

    if (!url) {
      fetchErrors.forEach(e => console.error(e));

      throw new Error('Failed to fetch. See console');
    }

    const a = document.createElement('a');

    a.href = url;
    a.download = `${name}${fileExt}`;
    document.body.appendChild(a); // is needed due to firefox bug
    a.click();
    a.remove();

    window.URL.revokeObjectURL(url);
  }
}());
§
Posted: 2023-12-19

Finally it works very well with this version 2.1.5b, here's the proof in a screenshot :-)

image

In additon, do you think that it's also possible to add the version number directly to the backup please ?

-> Remove YouTube Movie Purchase 1.0.1.user.js instead of Remove YouTube Movie Purchase.user.js

-> Remove YouTube Movie Purchase 1.0.1.user.css instead of Remove YouTube Movie Purchase.user.css

This way it's direct and you don't have to do it manually every time because it saves time...

KonfAuthor
§
Posted: 2023-12-19

Yeah, makes sense. I'll see how this could be done

KonfAuthor
§
Posted: 2023-12-19

2.2.6 has been released. Thanks for your help

§
Posted: 2023-12-20

Very good, your script is really much better and efficace now and I hope it helps everyone including you.

Thank you very much for your time, work, understanding and kindness ;-)

Post reply

Sign in to post a reply.