Добавление пользовательского меню CSS в Slack


Введение

Многим людям не нравится, как выглядит/работает Discord, поэтому существует множество модов, которые добавляют в него возможность настройки:

  • BetterDiscord
  • PowerCord (сейчас не существует)
  • GooseMod (мой личный фаворит)

Slack — это, по сути, ухудшенный Discord, но ориентированный на профессионалов, а не на геймеров.

Учитывая, что Slack хуже, можно ожидать, что для него будет больше модов. Несмотря на это ожидание, результаты моего поиска мода для Slack были ошеломляющими. Нет абсолютно никаких клиентов, которые делают это за вас, а все учебники по внедрению css/js не работают на современных версиях Slack.

  • 2019 Статья на Medium
  • Github о пользовательском CSS последнее обновление 2020 г.
  • Комментарий на reddit от 2019 года

Единственный способ, который я нашел до сих пор, который работает сейчас, — это инжектор электронов общего назначения на github. Но это далеко от простоты использования модов клиента Discord, таких как GooseMod.

Внедрение JavaScript с помощью Electron Inject

Вместо того чтобы изобретать колесо, я решил сделать свой проект зависимым от хорошо поддерживаемого github-репозитория electron-inject.

Вот сценарий, который я создал

from electron_inject import inject

# for windows this will be something like "C:/ProgramData/F53/slack/app-4.27.154/slack.exe"
slack_location = "/usr/lib/slack/slack"
inject_location = "/home/f53/Projects/SlackMod/inject.js"

inject(slack_location, devtools=True, timeout=600, scripts=[inject_location])
Вход в полноэкранный режим Выйти из полноэкранного режима

Разработка с помощью этого боль очень просто:

  • Внесите изменения в свой файл javascript
  • перейдите на вкладку slack и нажмите alt f4.
    • убедитесь, что у вас настроен slack, чтобы он не запускался в фоновом режиме
  • alt tab в консоли, нажмите вверх и enter, чтобы повторно запустить скрипт python.

Для меня инжектор, заставляющий F12 открыть devtools, не сработал. К счастью, в slack есть встроенная команда /slackdevtools.

Это должно было быть временным решением для меня, пока я делал этот мод. Поскольку сроки сдачи этого блога приближаются, я сделаю свой собственный автоматический инжектор как-нибудь позже.

Добавление раздела Custom CSS в меню Preferences

Цель:

В конечном счете, я хочу, чтобы вкладка пользовательского CSS была похожа на раздел сниппетов Topaz, где по сути есть выборка файлов для редактирования/включения/отключения отдельных css файлов.

Но в настоящее время я понятия не имею, как сохранить файл, поэтому цель на сегодня — что-то похожее на Custom CSS от Goosemod, где есть один редактор.

Первоначальный план

Чтобы добавить новое меню, я сначала получил селектор для списка вкладок, который я буду добавлять. Просмотрев HTML, я определил, что класс p-prefs_dialog__menu является хорошим вариантом, так как он короткий и не используется в других местах.

const settingsTabList = document.querySelector(".p-prefs_dialog__menu")
Вход в полноэкранный режим Выйти из полноэкранного режима

Отсюда я написал код для базового теста

const settingsTabList = document.querySelector(".p-prefs_dialog__menu")
// make a button
customTab = document.createElement("button")
// Set it's label
customTab.innerHTML = `<span>Custom Tab!</span>`
// add the button to the list we selected
settingsTabList.appendChild(customTab)
Вход в полноэкранный режим Выйти из полноэкранного режима

Я ожидал получить результат примерно такой

Проблема заключалась в том, что буквально ничего не происходило. Сколько бы журналов я не добавил, я даже не смог найти, когда мой код был запущен.

Теоретически этот код должен работать, если все загружено, поэтому давайте поместим его в функцию на потом.

function addSettingsTab() {
    const settingsTabList = document.querySelector(".p-prefs_dialog__menu")
    customTab = document.createElement("button")
    customTab.innerHTML = `<span>Custom Tab!</span>`

    settingsTabList.appendChild(customTab)
}
Войти в полноэкранный режим Выход из полноэкранного режима

Работа с DOM, полностью созданным с помощью JS

Оказывается, 100% HTML, который есть в slack, добавляется javascript. Из-за этого, заставить любой JS, связанный с DOM, работать на самом деле было большой проблемой. Потребовалось несколько часов, чтобы разобраться в этом, и еще больше времени, чтобы понять, как это объяснить.

Чтобы убедиться, что мы добавляем вкладку, когда открыт экран предпочтений. Мы могли бы сделать это после нажатия кнопки предпочтений.

// const preferencesButton = querySelector(selector)
preferencesButton.addEventListener("click", (event) => {
    addSettingsTab()
})
Войти в полноэкранный режим Выйти из полноэкранного режима

Но подождите, эта кнопка предпочтений также находится во всплывающем меню, мы не можем выбрать ее!

Чтобы убедиться, что меню, в котором она находится, открыто, мы можем убедиться, что пользователь навел курсор на всплывающее меню

// const userPopout = querySelector(selector)
userPopout.addEventListener("hover", (event) => {
    // const preferencesButton = querySelector(selector)
    preferencesButton.addEventListener("click", (event) => {
        addSettingsTab()
    })
})
Вход в полноэкранный режим Выйти из полноэкранного режима

Но подождите, на некоторых экранах всплывающее окно пользователя не отображается!

Возможно, вы заметили здесь закономерность: мы не можем иметь никаких прямых селекторов к элементам, если не знаем, что находимся в контексте, в котором они существуют.

Возможно, существуют сотни решений этой проблемы, но вот одно из них, которое придумал я.

// cant safely select any HTML, so select root
document.addEventListener("click", (event) => {
    // check if clicked element is the button that opens the preferences window
    let element = event.target 
    if (element.classList[0]=="c-menu_item__label" && element.innerHTML == "Preferences") {
        addSettingsTab()
    }
})
Войти в полноэкранный режим Выйти из полноэкранного режима

Это почти работает, но есть еще одна техническая особенность. Наш инжектированный код запускается раньше кода Slack.

Вот эффективный порядок действий в псевдокоде

injected code:
    document.click {
        clicked == preferencesButton {
            preferencesScreen.append(button)
        }
    })

slack's code:
    preferencesButton.click {
        make preferencesScreen
    })
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы пытаемся добавить кнопку на экран до того, как она будет сделана!

Мы можем исправить это, добавив асинхронную команду ожидания в наш код, выполняемый после создания экрана, что устранит проблему.

// setTimeout(function to run, how long from now to run it in milliseconds)
setTimeout(function () { 
    if (document.querySelector(".p-prefs_dialog__menu") != null) {
        addSettingsTab()
    }
}, 500);
Вход в полноэкранный режим Выход из полноэкранного режима

Вот «псевдокод» для этого, левая панель — поток 1, правая панель — поток 2.

В общем, для всего этого нужно проверить, будет ли открыт экран, подождать, пока экран откроется, отредактировать экран.

Вот весь этот код полностью

// cant safely select any HTML, so select root
document.addEventListener("click", (event) => {
    // check if clicked element is the button that opens our screen
    let element = event.target 
    if (element.classList[0]=="c-menu_item__label" && element.innerHTML == "Preferences") {
        // Our injected code is runs before Slack's code.
        // So the screen hasn't been made yet
        // Wait a short bit asynchronously so it can be made
        // Then add it.
        setTimeout(function () {
            if (document.querySelector(".p-prefs_dialog__menu") != null) {
                addSettingsTab()
            }
        }, 500);
    }
})
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление текстового редактора во вкладку

Вот план:

function addSettingsTab() {
    const settingsTabList = document.querySelector(".p-prefs_dialog__menu")
    customTab = document.createElement("button")
    customTab.innerHTML = `<span>Custom CSS</span>`
    // add class that make look good

    // onClick
    //     deselect old tab
    //     select new tab
    //     clear pane to the right
    //     add some kind of multiline text form to the pane

    settingsTabList.appendChild(customTab)
}
Войти в полноэкранный режим Выход из полноэкранного режима

Добавьте класс, который будет выглядеть хорошо:

  • скопируйте все классы с одной из других кнопок
  • установите пользовательскую вкладку, чтобы иметь их
// add class that make look good
customTab.classList = "c-button-unstyled c-tabs__tab js-tab c-tabs__tab--full_width"
Войти в полноэкранный режим Выйти из полноэкранного режима

Честно говоря, я не ожидал, что это будет так просто, и поэтому я сделал это отдельным шагом.

Визуальное выделение вкладок зависит от того, есть ли у них класс c-tabs__tab--active. Поэтому процесс отмены выбора/выделения должен быть примерно таким:

const activeClass = "c-tabs__tab--active"
// get old tab
let activeTab = settingsTabList.querySelector("."+activeClass)
// visually deselect old tab by removing class
activeTab.classList = // classList but without activeClass
// visually select new tab by adding class
customTab.classList = customTab.classList.toString() + " " + activeClass
Войти в полноэкранный режим Выход из полноэкранного режима

Удаление выбранного класса сначала кажется довольно сложной задачей, поскольку по умолчанию activeTab.classList является массивом какого-то пользовательского объекта, но вызов activeTab.classList.toString() позволяет нам просто использовать .replace(stringToRemove, "").

Это дает нам следующее:

// visually deselect old tab by removing class
activeTab.classList = activeTab.classList.toString().replace(activeClass+" ", "")
// visually select new tab by adding class
customTab.classList = customTab.classList.toString() + " " + activeClass
Вход в полноэкранный режим Выйти из полноэкранного режима

Это выглядит довольно хорошо!

Сделать текстовый редактор очень просто

// make the element
let cssEditor = document.createElement("textarea")
// size it 
cssEditor.setAttribute("rows", "33")
cssEditor.setAttribute("cols", "60")
Вход в полноэкранный режим Выйти из полноэкранного режима

Остальная часть нашего текущего плана может быть выполнена в 1 строку. По сути, мы просто выкидываем все, что показывала старая вкладка Preferences, и вставляем нашу текстовую область

// clear pane to the right
// add some kind of multiline text form to the pane
document.querySelector(".p-prefs_dialog__panel").replaceChildren(cssEditor)
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь у нас есть текстовый редактор!

Фактическое использование ввода как CSS

Этот ответ на stackoverflow отлично подходит для добавления произвольного CSS, не специфичного для узла. Для наших целей мы используем его в общих чертах:

// make a new style element for our custom CSS
let styleSheet = document.createElement("style")
// set default contents of Custom CSS
styleSheet.innerText = "/*Write Custom CSS here!*/"
// give it an id to make it easier to query
// the document for this stylesheet later
styleSheet.id = "SlackMod-Custom-CSS"
// add to head
document.head.appendChild(styleSheet)
Войти в полноэкранный режим Выход из полноэкранного режима

Мы не можем просто сделать styleSheet.innerText = new value, потому что styleSheet — это статическая ссылка. Вместо этого мы запрашиваем у документа ID, который мы ему присвоили, а затем устанавливаем css оттуда:

document.querySelector("#SlackMod-Custom-CSS").innerText = newCSS;

Чтобы сделать это чище, я сделал 2 метода

// method to quickly change css
const updateCustomCSS = newCSS => { document.querySelector("#SlackMod-Custom-CSS").innerText = newCSS; }
// method to quickly get inner css
const getCustomCSS = () => { return document.querySelector("#SlackMod-Custom-CSS").innerText}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь нам просто нужно сделать так, чтобы наш cssEditor выполнял их соответствующим образом

// a big proper editor
let cssEditor = document.createElement("textarea")
cssEditor.setAttribute("rows", "33")
cssEditor.setAttribute("cols", "60")
// set content from current CSS
// on new chars added
    // update current CSS
Войти в полноэкранный режим Выйти из полноэкранного режима

Настроить содержимое очень просто

cssEditor.setAttribute("rows", "33")
cssEditor.setAttribute("cols", "60")
// set content from current CSS
cssEditor.value = getCustomCSS()
Вход в полноэкранный режим Выход из полноэкранного режима

Найти, какой слушатель событий должен обнаруживать, когда в редактор добавляются новые символы, было сложнее, но, прочитав список всех слушателей событий, я нашел то, что мне нужно — ужасно названное событие «input».

Судя по его описанию, это именно то, что нам нужно:

Событие input срабатывает при изменении значения элемента <input>, <select> или <textarea>.

Но его имя input, не inputChanged или что-то описательное, а просто input.

Как только мы узнаем ужасно плохое имя нужного нам слушателя событий, сделать обновление css в реальном времени будет проще простого:

// on new chars added
cssEditor.addEventListener("input", ()=>{
    // update current CSS
    updateCustomCSS(cssEditor.value)
})
Войдите в полноэкранный режим Выйти из полноэкранного режима

Вот теперь мы двигаемся!

Теперь мы можем писать css и видеть, как он обновляется по мере ввода!

Заставляем редактор CSS вести себя и выглядеть правильно

Однако есть несколько вещей, которые мешают этой пользовательской панели CSS быть удобной для использования.

  • табуляция выводит вас за пределы поля вместо отступов
  • шрифт не моноширинный
  • при выходе и повторном входе на экран все новые строки исчезают.
    • не знаю, почему так происходит и как это исправить.

Исправить вылетающую вкладку очень просто, достаточно запретить поведение клавиши табуляции по умолчанию, когда вы находитесь в редакторе css.

// make pressing tab add indent
cssEditor.addEventListener("keydown", (event) => {
    if (event.code == "Tab") {
        event.preventDefault();
    }
})
Вход в полноэкранный режим Выйти из полноэкранного режима

Однако я не знаю, как сделать так, чтобы отступы действительно были.

Чтобы решить проблему с моноширинным шрифтом, я просто изменил значение по умолчанию поля CSS на это вместо /*Write Custom CSS here!*/.

/*Write Custom CSS here!*/
.p-prefs_dialog__panel textarea {
   font-family: Monaco,Menlo,Consolas,Courier New,monospace!important;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Таким образом, редактор стал в некоторой степени пригодным для использования.

Я добавил еще немного css, чтобы сделать редактор более приятным в использовании.

/*Write Custom CSS here!*/
/* Improve Legibility of Custom CSS Area */
.p-prefs_dialog__panel textarea {
    font-family: Monaco, Menlo, Consolas, CourierNew, monospace!important;
    font-size: 12px;
    /* Make editor fill Preferences panel */
    width: 100%; 
    height: calc(100% - 0.5rem);
    /* disable text wrapping */
    white-space: nowrap;
    /* make background of editor darker */
    background-color: #1c1c1c;
}
/* Increase width of Preferences to allow for more code width */
body > div.c-sk-modal_portal > div > div {
    max-width: 100%!important;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Текущие ограничения

В настоящее время этот мод ограничен несколькими способами:

  • CSS не сохраняет новые строки
  • CSS не сохраняется между перезагрузками
    • при изучении файлового ввода/вывода Javascript, очевидно, нет прямого способа сохранить файл в системе пользователя.
  • Инъекции выполняются вручную и жестко закодированы в системе пользователя.
    • Каждый раз, когда я изменял внедряемый javascript во время написания этого блога, я
      • alt+f4’d slack
      • переходил в терминал
      • нажимал вверх и enter для повторного запуска python slack_launch.py
      • подождал ~30 секунд
  • Выбор пользовательского css в настройках, а затем выбор другой категории приводит к проблеме
  • Нет подсветки синтаксиса

Обычно я откладываю выпуск блога до тех пор, пока все эти проблемы не будут исправлены, но у меня есть крайний срок для выпуска этого блога.

Если вы хотите помочь в решении этих проблем, весь обсуждаемый код можно найти на github.com/CodeF53/SlackMod. В противном случае, следите за продолжением, в котором я исправлю эти проблемы.

Оцените статью
devanswers.ru
Добавить комментарий