Нюансы про вес, которые я понял на своем опыте

Предыстория

После 23 лет у меня начались проблемы с лишним весом.

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

Попытки похудеть были всегда: интервальное голодание, голодание на несколько дней, попытки есть до 18:00, есть сладкое до обеда — все было. Ничего не помогло.

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

В первый раз я похудел на 8 кг в Таиланде. Сейчас я похудел в Узбекистане на 5 кг и постепенно прихожу к тому виду тела, которое я хочу.

Главное

У меня успешно получается снижать вес, когда я понимаю зачем. Важна истинная причина.

Я похудел на 8 кг в Таиланде, после того, как увидел свои анализы и когда врач сказал: «У тебя 2 стадия ожирения. Не похудеешь, словишь диабет и ещё кучу болячек». Мне стало дико страшно. Ведь я хочу жить, а не бороться с болезнями остаток жизни. Но это плохой стимул. Потом я все равно жрал шоколадки и прочую дрянь как не в себя.

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

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

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

Нюансы про вес, которые я понял на своем опыте 1
Нюансы про вес, которые я понял на своем опыте 2

Разница 2 месяца. 85 кг → 79кг

Нюансы про вес

1. Важно отделить питание от тренировок

Как было у меня. В один момент надоедает свой вид в зеркале и я такой: «Погнали в ЗОЖ». Начинаю тренироваться и питаться нормально. Потом в какой-то сложный день я забиваю на тренировку или получаю травму. И сразу автоматически слетает питание.

То есть эти процессы в голове связаны. Их надо развязать. Я сначала 2 месяца выстраивал питание, потом подключил тренировки. Когда нормально питаешься, теряешь вес, энергия приходит сама и хочется какой-то суеты навести.

2. Тренировки не приведут к похудению

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

Питание — для поддержания фигуры и здорового тела. Тренировки — для мышц и укрепления тела.

3. Цифры на весах не отражают жир

Спасибо кэп, как говорится. Но когда я раньше вставал на весы, и если вчера весил 81 кг, а сегодня вешу 80.5кг я такой: «Ееее бой! Я скинул 500г». Хотя что это было? Вода, жир, мышцы — да хрен с ним. Не суть важно. Главное убавилось. То есть на подсознании восприятие, что это жир ушел.

Я проверил это на себе. У меня есть фото, когда я вешу 72, 74, 76, 78, 80, 82, 84, 85. Процесс падения на дно задокументирован 😂️️. Если вообще не заниматься спортом, то цифра косвенно отражает убавился ли жир или нет. Но это с большой оговоркой. Но когда занимаешься спортом, цифре вообще верить нельзя.

4. Нормальное питание не временно, оно навсегда

Я часто воспринимал это как диету. Вот я на ПП — месяц. А потом привыкну. И если это не заканчивалось раньше, то это точно заканчивалось в указанный дедлайн.

В этот раз я сказал себе: «Даня, дедлайна нет, это навсегда, до конца твоей жизни. Встал на эту дорожку и иди». Как и с курением. Когда я бросал курить, я не говорил: «Я некурящий … этот год». Я не перестаю курить на время, я перестал навсегда.

Это не диета, не ПП, не что-то ещё. Это мой новый режим питания. Не нормально, это когда я вдруг съел какую-то дрянь. Базовая линия питания — нормальная. Где нет шоколадок, пирожных, чипсов, алкоголя и прочей дряни.

5. Излишний контроль калорий надоедает

Кроме этого раза я всегда считал калории. Это всегда бесит. У тебя лежит на столе просто куча всего. И ты тратишь рутинные 15 минут, чтобы все занести. Я всегда забивал на это. У меня нет столько дисциплины, чтобы этим заниматься.

Я пошел другим путем, стал есть примерно одинаковую еду каждый день. Иногда разбавлять какими-то отличающимися блюдами. И перестал есть, когда не голоден. Это очень сложно понять. Я выработал привычку так: если в желудке посасывает, пью 3 стакана воды. Если потом хочу есть — иду есть. Если пропало ощущение — значит это была обманка.

6. Еда — это топливо

Я всю жизнь относился к еде так: «уууууух сейчас как поедим вкусненько». И это привело меня к ожирению. В последние два захода на выправление питания я принял правило: «Еда — топливо».

Ты не должен ух как кайфовать от еды. Все эмоции — за пределами стола: от достижений, секса, общения, осознаний, развития, от остальной жизни. И это даёт плоды. Я отношусь к еде ровно, она не вызывает у меня истерики и нет такого, что я не могу себя контролировать.

Вот и всё. Надеюсь, эти нюансы помогут вам в борьбе с весом. Если пост был полезен — ставьте лайк и делитесь с друзьями 👍

7. Отлеживайте прогресс

Даже если вы не сильно меняетесь — фотографируйте свое тело и складывайте в отдельный альбом на телефоне. Это может не пригодится в моменте, но возможно приведет к какому-то осознанию, пониманию что и как влияет на твоё тело. Я осознал про цифры на весах, когда увидел разные фото с разным весом и разным соотношением мышц-жира.

Чего я боюсь

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

Чем я питаюсь

Курица, рыба, овощи, картошка фри. 3−4 чашки чая и кофе в день. Мой завтрак, обед и ужин выглядит вот так последние 2 месяца.

Нюансы про вес, которые я понял на своем опыте 3

Как готовлю

Купил аэрогриль за $ 80. Загружаю туда рыбу и фри на 20 минут, ухожу работать. Готовится без масла, не надо никак контролировать. Моется легко. Идеально для холостяка.

Нюансы про вес, которые я понял на своем опыте 4

Как я выглядел в разные периоды

И почему смотреть на весы, когда тренируешься — обманывать себя.

2014 год. Армия: строгое питание, физуха каждый день. 70 кг.

Нюансы про вес, которые я понял на своем опыте 5

2017 год. Не тренируюсь, ем все, что не приколочено. Вес 72 кг.

Нюансы про вес, которые я понял на своем опыте 6

2022 год. Ем всякую дрянь, не тренируюсь. Вес 80кг

Нюансы про вес, которые я понял на своем опыте 7

2023 год. Занимаюсь силовыми один раз в неделю. Ем нормальую еду. Вес 72 кг.

Нюансы про вес, которые я понял на своем опыте 8

2024 год. Ем дрянь, не тренируюсь. Вес 85кг

Нюансы про вес, которые я понял на своем опыте 9

2024 год. Хожу на бокс, занимаюсь на турниках. Ем нормальную пищу. Вес 79 кг.

Нюансы про вес, которые я понял на своем опыте 10

Документация по конфигуратору Yougile

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

Как планирую использовать этот конфигуратор — наполню его, отдам все Claude AI, чтобы иметь своего разработчика по скриптам Yougile.

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

Разработчики Yougile, если вы это читаете — пожалуйста выгрузите все методы и переменные API конфигуратора, чтобы её можно было просто скопировать одним кликом или скачать файлом.

Что такое конфигуратор?

Конфигуратор — это среда, в которой можно создавать javascript-код, который будет дополнять функциональность системы Yougile.

Полезные ссылки

Навигатор по API

Notifier

// Notifier.error(message)
// Показывает нотификацию ошибки.
Notifier.error('Task was added successfully');

// Notifier.warn(message)
// Показывает нотификацию предупреждения.
Notifier.warn('This task is readonly!');

// Notifier.success(message)
// Показывает нотификацию успешно выполненного действия.
Notifier.success('Task was added successfully');

//Notifier.show(title, message, onClick)
//Показывает браузерную нотификацию.
Notifier.show('New message', 'Hi, how’s it going?');

// Notifier.playSound(soundNumber)
// Проигрывает звук. Доступны 7 вариантов звуков.
// Звук по умолчанию, как в чатах
Notifier.playSound(); 
// Другой звук (доступно 7 вариантов)
Notifier.playSound(2);

Current

// Current.user
// Текущий пользователь (см. “user variable”).
if (Current.user.name === 'John') {
    console.log('Hi John!');
}

// Current.project
// Открытый проект (см. “item variable”). Только для чтения.
var project = Current.project;
console.log('Открытый проект:', project);

// Current.board
// Открытая доска (см. “item variable”). Только для чтения.
var board = Current.board;
console.log('Открытая доска:', board);

// Current.chat
// Открытый чат задачи (см. “item variable”). Только для чтения.
var chat = Current.chat;
console.log('Открытый чат задачи:', chat);

// Current.company
// Возвращает компанию, в которой запускается скрипт.
var company = Current.company;
console.log('Company is ' + company.name);

// Current.onProjectChange(oldProject, newProject)
// Эта функция вызывается, когда пользователь переходит в другой проект.
Current.onProjectChange = function (oldProject, newProject) {
    if (newProject) {
        console.log('Navigated to ' + newProject.name);
    } else {
        console.warn('Exited project');
    }
};

// Current.onBoardChange(oldBoard, newBoard)
// Эта функция вызывается, когда пользователь переходит в новую доску.
Current.onBoardChange = function (oldBoard, newBoard) {
    console.log('Board ' + newBoard.name + ' opened!');
};

// Current.onChatChange(oldChat, newChat)
// Эта функция вызывается, когда открывается чат по другой задаче.
Current.onChatChange = function (oldChat, newChat) {
    console.log('Opened chat for ' + newChat.name + ' task!');
};

Items

Позволяет манипулировать всеми объектами внутри YouGile (задачи, колонки, доски, проекты) и описывает поведение по умолчанию для этих объектов. Проекты, борды, колонки и задачи структурированы в некую иерархичную систему, похожую на файловую систему компьютера (смотри вкладку «Навигатор по объектам»). «Items» позволяет получить доступ и управлять ими на этот манер. И когда вы обращаетесь к какой-либо задаче или доске через Items, вы получаете переменную типа item (см. «item variable»).

Items.get (id)

Получает объект YouGile по его id. Объекты YouGile: компания, проекты, доски, колонки, задачи. Вы можете найти id объектов во вкладке «Навигатор по объектам».

// Getting a task and assigning it to variable “t” for later use
var t = Items.get('bb12a559-81ba-40ad-adda-81f2d017e71b');

// Appending “!” to the task’s name
t.name = t.name + '!';

Items.onAdd (newObject)

Эта функция вызывается сразу после добавления нового объекта в систему (задачи, колонки, доски, проекта).

Items.onAdd = function (obj) {
    if (obj.type === 'Task') { // only when a task was added
        // Changing the name of the new task
        obj.name = obj.name + ' ...';
    }
};

Items.onBeforeAdd (location, name)

Эта функция вызывается, перед добавлением нового объекта в систему. Первый аргумент — куда добавляется новый объект (напр. если добавляется задача, то в первом аргументе колонка, куда добавляется), второй аргумент — имя нового объекта. Если функция возвращает false, добвление отменяется.

Items.onBeforeAdd = function (a, s) {
    if (a.type === 'Column') { // only when a task is added to a column
        // Prevent task addition with name “test”
        if (s === 'test') {
            return false;
        }
    }
    // We return true because we want to proceed with the addition
    // If it is needed to prevent addition, then return false
    return true;
};

Items.onRename (item, newName)

Эта функиця вызывается, когда какой-нибудь объект переименовывается (задача, колонка, доска или проект).

Items.onRename = function (obj, n) {
    if (obj.type === 'Task') { // only for task renaming
        obj.name = n + '!!!';
        // Return false to stop the default action — renaming the
        // object to a name specified by the user (n variable in our case)
        return false;
    }
    // Make the default action in all other cases
    return true;
};

Items.onDelete (item)

Эта функция вызывается, когда юзер удаляет какой-нибудь объект (задачу, колонку, доску или проект).

Items.onDelete = function (object) {
    // Preventing deletion of objects whose names start with 'Important'
    if (object.name.slice(0, 9) === 'Important') {
        Notify.warn('You’re not allowed to delete important things!');
        return false;
    }
    // Otherwise proceed with deletion
    return true;
};

Items.onClick (item)

Эта функция вызывается, когла пользователь щелкает на задачу, заголовок колонки, вкладку доски или на проект.

Items.onClick = function (object) {
    Notifier.success('You’ve clicked ' + object.name);
    return true;
};

Items.onList (item, items)

Эта функция вызывается, когда система показывает список объектов внутри какого-то объекта. Например, когда отрисовывается список задач внутри колонки или список колонок внутри доски и так далее.

Items.onList = function (obj, arr) {
    // obj - is a container which child objects are going to be listed
    // arr - is its actual children objects
    if (obj.type === 'Column') {
        // In case of task list in a column
        // Here obj is a column and arr is all its tasks
        // We return only 5 elements from the array of tasks,
        // this result is going to be shown to the user
        // So the user is going to see no more than 5 tasks in every column
        return arr.slice(0, 5);
    }
    // Otherwise, we return the default
    return arr;
};

Items.onMove (item, from, to)

Эта функция вызывается, когда пользователь перемещает объект из одного места в другое (например, перемещает задачу из одной колонки в другую).

Items.onMove = function (obj, from, to) {
    // Getting some board by id
    var brd = Items.get('... board id here');
    
    // Suppose we want to disable moving tasks in this board
    // So we check if the parent object of 'from' is this board
    // If so, then 'from' object is some column inside this board
    // and that’s why 'obj' is some task inside this column which the user is
    // trying to move somewhere. So we return false to prevent it.
    if (from.up().id === brd.id) {
        Notifier.warn('Moving tasks inside ' + brd.name + ' board is disallowed');
        return false;
    }
    
    return true;
};

Items.onUpdate (item)

Эта функция вызывается когда какой-то объект системы был обновлён (проект, доска, колонка, задача). Учитываются не только обновления текущим пользователем, но и другие обновления, приходящие с сервера. Если произошло обновление чата, то вызывается эта функция с соответсвующей задачей в качестве аргумента.

var column = Items.get('...');

// Let’s store the number of tasks
// in this column in some variable
var tasksCount = column.list().length;

// If we want to recalculate tasksCount
// on changes, then we can do it as follows
Items.onUpdate = function (obj) {
    if (obj.id === column.id) {
        tasksCount = column.list().length;
    }
};

Items.isHidden (item)

Items.isHidden = function (x) {
    // We do not want to show some column to user Mike
    if (Current.user.email === 'mike@example.com') {
        // Better to compare id
        if (x.id === '... here goes column id') {
            return true;
        }
    }
    return false;
};

Items.allowDelete (item)

Если эта функция возвращает false для какого-нибудь объекта в какой-нибудь ситуации, то кнопка удаления не показывается для этого объекта (задачи, колонки, доски или проекта).

Items.allowDelete = function (object) {
    if (object.type === 'Column') {
        // Hide all delete buttons from all columns
        return false;
    }
    return true;
};

Items.allowAdd (item)

Если эта функция возвращает false для какого-нибудь объекта, тогда кнопка добавления не отображается внутри этого объекта.

Items.allowAdd = function (a) {
    // Remove “Add a new task” button from all columns
    if (a.type === 'Column') {
        return false;
    }
    return true;
};

Items.allowRename (item)

Если эта функция возвращает false для некоторого объекта, тогда он не может быть переименован через интерфейс.

Items.allowRename = function (object) {
    // Disallow renaming to a specific user
    if (Current.user.id === '... user id here') {
        return false;
    }
    return true;
};

Items.allowDrag (item)

Если эта функция возвращает false для объекта, то его нельзя перетаскивать.

Items.allowDrag = function (object) {
    // Disallow dragging for a specific user
    if (Current.user.id === '... user id here') {
        return false;
    }
    return true;
};

Примеры скриптов от разработчиков

Работа Javascript в чате

/**
 * Выполняет javascript в чате,
 * если сообщение начинается с >>>,
 * то оно выполняется как javascript.
 * Пример:
 * >>> 12 * 12;
 *  » 144
 */

Chat.onPostMessage = function (msg) {
  if (msg.text.slice(0, 3) === '>>>') {
    var res = eval(msg.text.slice(3));
    msg.text += 'n » ' + res;
    msg.setData({js: true});
    highlight(msg.getTask());
  }
};

// подсвечивает сообщения в чате с выполненным js
function highlight(chat) {
  Chat.listMessages(chat).forEach(function (msg) {
    if (msg.getData().js) {
      msg.ui.style = {
        fontFamily: 'Menlo, Monaco, Courier New, monospace',
        background: '#fffded'
      };
    }
  });
}

// подсвечиваем сообщения с js при старте скрипта
// и при открытии нового чата
if (Current.chat) {
  highlight(Current.chat);
}
Current.onChatChange = function (oldChat, newChat) {
  if (newChat) {
    highlight(newChat);
  }
};

Добавляет кнопку «время ответа» в панель стикеров досок.


/**
 * Добавляет кнопку “время ответа” в панель стикеров досок.
 * Если кнопка нажата, то на всех задачах доски отображается
 * время ответа на сообщения в чате.
 *
 * Чтобы время считалось, нужно, чтобы на задачу был назначен
 * ответственный (стикер пользователь). Тогда время ответа —
 * это среднее время, за которое отвественный пользователь
 * отвечает другим пользователям в чате.
 *
 * Если назначенный на задачу пользователь не ответил на
 * последнее сообщение, задача посвечивается красным.
 */

// добавим кнопку “время ответа” в панель стикеров
var trigger = UI.text('время ответа');
UI.components.stickerPanel.add(trigger);
trigger.style = {
  fontSize: '12px',
  lineHeight: '16px',
  padding: '0px 10px 1px',
  display: 'inline-block',
  borderRadius: '10px',
  cursor: 'pointer',
  border: '1px solid #fff',
  verticalAlign: 'top',
  marginTop: '3px',
  marginRight: '6px',
  color: '#fff',
  background: 'rgba(100, 100, 100, 0.4)'
};

// эта кнопка включает/выключает отображение времени ответа в доске
trigger.onClick = function () {
  var data = Current.board.getData();
  data.responseTimes = !data.responseTimes;
  Current.board.setData(data);
  redraw();
};

redraw(); // redraw отрисовывает время ответов в задачах доски см. ниже
Current.onBoardChange = redraw;

// при обновлении задач из доски, пересчитаем время ответов
Items.onUpdate = function (item) {
  if (!Current.board || !Current.board.getData().responseTimes) {
    return;
  }
  if (item.type === 'Task' && item.up().up().id === Current.board.id) {
    updateTask(item);
    redrawTask(item);
  }
};

// отрисовывает время ответов в задачах доски
function redraw() {
  if (!Current.board) {
    return;
  }
  var enabled = Current.board.getData().responseTimes;
  Current.board.list().forEach(function (col) {
    col.list().forEach(function (task) {
      if (enabled) {
        updateTask(task);
        redrawTask(task);
      } else {
        task.ui.clear();
      }
    });
  });
  var enabled = Current.board.getData().responseTimes;
  if (enabled) {
    trigger.style.background = 'rgba(70, 200, 50, 0.6)';
    trigger.refresh();
  } else {
    trigger.style.background = 'rgba(100, 100, 100, 0.4)';
    trigger.refresh();
  }
}

// обновляет время ответа в задаче
function updateTask(task) {
  var assigned = Users.getAssigned(task);
  if (!assigned) {
    return;
  }
  var data = task.getData();
  if (data.lastMessageTime &&
        data.lastMessageTime >= lastMessageTime) {
    return;
  }
  var msg = Chat.listMessages(task);
  if (msg.length === 0) {
      return;
  }
  var count = 0;
  var totalTime = 0;
  var lastQuestionTime = 0;
  var lastMessageTime = 0;
  var asked = false;
  for (var i = 0; i < msg.length; i++) {
    var m = msg[i];
    if (m.from.name === '_') { // YouGile system message
      continue;
    }
    lastMessageTime = m.timestamp;
    if (m.from.id !== assigned.id) {
      if (!asked) {
        lastQuestionTime = m.timestamp;
        asked = true;
      }
    } else {
      if (asked) {
        totalTime += m.timestamp - lastQuestionTime;
        asked = false;
        count++;
      }
    }
  }
  if (count > 0 ||  asked) {
    var result = Math.round(totalTime / count / 1000);
    data.responseTime = result;
    data.noResponse = asked;
    data.lastMessageTime = lastMessageTime;
    task.setData(data);
  }
}


// переписовывает всемя ответа в задаче
function redrawTask(task) {
  task.ui.clear();
  var assigned = Users.getAssigned(task);
  if (!assigned) {
    var warn = UI.text('(пользователь не назачен)');
    warn.style = {
      fontSize: '13px',
      color: '#999'
    };
    task.ui.add(warn);
    return;
  }
  var time = task.getData().responseTime;
  var noResponse = task.getData().noResponse;
  if (time || noResponse) {
    var txt = UI.text(formatTime(time));
    txt.style = {
      fontSize: '13px',
      color: time < 3600 ? '#999' : '#e77'
    };
    if (noResponse) {
      task.ui.style.background = '#ffede5';
      task.ui.refresh();
    }
    if (time) {
      task.ui.add(txt);
    }
  }
}

// конвертирует количество секунд в
// удобный для чтения формат, напр.: 200 => 3m 20s
function formatTime(sec) {
  var s = sec % 60;
  var m = (sec - s) / 60;
  var h = (m - m % 60) / 60;
  m = m % 60;
  var d = (h - h % 24) / 24;
  h = h % 24;
  var parts = [];
  parts.push(s + 's');
  if (m) {
    parts.push(m + 'm');
    if (h) {
      parts.push(h + 'h');
      if (d) {
        parts.push(d + 'd');
      }
    }
  }
  parts.reverse();
  return parts.join(' ');
}

Настраивать свой фон для каждой доски отдельно.

/**
 * Позволяет настраивать свой фон для каждой доски отдельно.
 * Интерфейс изменения фона виден администраторам.
 * Интерфейс открывается по нажатию Ctrl+1
 */

if (Current.user.isAdmin) {
  Notifier.success('Нажмите Ctrl+1 чтобы открыть настройку фона');
}

// интерфейс настройки фона
var dialog = UI.panel(); // панель выбора фона
dialog.style.display = 'none'; // сначала не видна, нужно нажать Ctrl+1
var allBg = UI.panel(); // здесь будут уже загруженные фоны
var addBgBtn = UI.button('Загрузить новый фон');
var cancelBtn = UI.button('Отмена');
var clearBtn = UI.button('Очистить фон');
// добавляем все кнопки в панель
dialog.add(addBgBtn);
dialog.add(cancelBtn);
dialog.add(clearBtn);
dialog.add(allBg);

// события при нажатии на кнопки
cancelBtn.onClick = closeDialog;
clearBtn.onClick = function () {
  setBg('');
};
addBgBtn.onClick = function () {
  closeDialog();
  App.chooseAndUpload(function (url) {
    var data = Current.company.getData();
    data.backgrounds = data.backgrounds || [];
    data.backgrounds.push(url);
    Current.company.setData(data);
    setBg(url);
  });
};

// по нажатию Ctrl+1 показываем панель настройки
Events.onKeyDown = function (e) {
  if (e.keyCode === 49 && Events.isCtrlPressed()) { // Ctrl+1
    // чтобы Ctrl+1 не перехватывалось системными комбинациями клавиш
    e.preventDefault();
    if (!Current.board) {
      Notifier.warn('Зайдите в доску, чтобы установить её фон ' +
        'и тогда нажмите Ctrl+1 ещё раз!');
      return;
    }
    if (dialog.style.display === 'none') {
      openDialog();
    } else {
      closeDialog();
    }
  }
};

// открытие интерфейса настройки
// и перерисовка списка загруженных фонов
// (возможно, кто-то добавил фон)
function openDialog() {
  dialog.style.display = 'block';
  dialog.refresh();
  allBg.clear();
  // добавляем загруженные фоны
  var all = Current.company.getData().backgrounds || [];
  all.forEach(function (url) {
    var prev = UI.image(App.getPreviewUrl(url));
    prev.style = imgStyle();
    allBg.add(prev);
    prev.onClick = function () {
      setBg(url);
      closeDialog();
    };
  });
}

// скрывает интерфейс настройки
function closeDialog() {
  dialog.style.display = 'none';
  dialog.refresh();
}

// устанавливает фон с заданным url в текущую доску
function setBg(url) {
  var boardData = Current.board.getData();
  boardData.bg = url;
  Current.board.setData(boardData);
  refreshBg();
}

// прикрепляет интерфейс настройки к текущей
// доске, но он при этом не виден, чтобы
// он показался используется openDialog();
function addBgChooser() {
  if (!Current.user.isAdmin || !Current.board) {
    return;
  }
  Current.board.ui.add(dialog);
}

// отрисовывает нужный фон,
// соответствуюший текущей доске
function refreshBg() {
  if (!Current.board) {
    return;
  }
  var bg = Current.board.getData().bg || '';
  UI.background = bg;
}

refreshBg();
addBgChooser();
Current.onBoardChange = function () {
  closeDialog();
  addBgChooser();
  refreshBg();
};

// styles
//
function imgStyle () {
  return {
    display: 'inline-block',
    margin: '10px',
    cursor: 'pointer',
    height: '100px',
    overflow: 'hidden',
    boxShadow: '0 1px 12px -2px #fff',
    borderRadius: '2px'
  };
};
function btnStyle() {
  return {
    background: 'none',
    color: '#fff',
    background: '#344',
    border: '1px solid #bbb',
    margin: '2px 4px 0 0'
  };
}
addBgBtn.style = btnStyle();
cancelBtn.style = btnStyle();
clearBtn.style = btnStyle();

Другие скрипты

// Скрипт: Убирает адресата из названия задачи в определенной колонке (для интеграции с почтой)
const column1 = "7c270d09-33a3-473c-8963-0b1c54a10f25"; // тут пишем нужную колонку
// Ниже можно написать и другие колонки по образцу выше
const columns = [column1]; // указываем эти колонки через запятую

function cutFirst(text) {
    return text.substr(text.indexOf(" ") + 1);
}

Items.onUpdate = function (obj) {
    // При обновлении
    if (columns.includes(obj.id)) {
        // Если колонка совпадает
        for (const task of obj.list()) {
            // Перебираем ее задачи
            if (task.name.includes("@")) {
                // Если есть @
                task.name = cutFirst(task.name); // Обрезаем название по первому пробелу
            }
        }
    }
};

// Скрипт: Множит по клику на карточку задачи через Shift задачи на исполнителей
Items.onClick = function (object) { // при клике
    if (object.type === 'Task') { // на задачу
        if (Events.isShiftPressed()) { // с зажатым Shift
            const originalName = object.name; // берем название
            const executors = Users.getAssigned(object, true); // исполнителей
            const checklists = Chat.getSubtasks(object); // чеклисты
            const description = Chat.getDescription(object); // описание
            const usersInChat = Chat.listUsers(object);

            function stickersInTask() { // функция для сбора стикеров в задаче
                const stickers = Stickers.getAll().filter(sticker => Stickers.isPinned(object, sticker));
                const values = stickers.map(sticker => ({
                    'key': sticker,
                    'value': Stickers.getValue(object, sticker)
                }));
                return values;
            }

            if (executors.length > 1) { // если несколько исполнителей
                Users.assignUser(executors[0], object); // оставляем в задаче первого исполнителя
                object.name = `[${executors[0].name}] ${originalName}`; // ставим его имя в название
                usersInChat.forEach(user => Chat.removeUser(object, user)); // чистим чат
                Chat.addUser(object, executors[0]); // подписываем
                executors.shift(); // удаляем из массива

                executors.forEach(executor => { // у остальных
                    object.up().add(`[${executor.name}] ${originalName}`); // добавляем по задаче
                    const currentTask = object.up().find(`[${executor.name}] ${originalName}`); // ищем ее
                    stickersInTask().forEach(sticker => { // для каждого стикера
                        setTimeout(function () { // таймаут для стабильности
                            Stickers.pin(currentTask, sticker.key, sticker.value); // ставим стикеры
                            Chat.setSubtasks(currentTask, checklists); // чеклисты
                            Chat.setDescription(currentTask, description); // описание
                            Users.assignUser(executor, currentTask); // исполнителя
                            usersInChat.forEach(user => Chat.removeUser(currentTask, user)); // чистим чат
                            Chat.addUser(currentTask, executor); // подписываем
                        }, 1000);
                    });
                });
            }

            return false; // не открываем задачу
        }
    }
    return true; // иначе открываем
};

// Скрипт: Добавляет к названию задачи названия проекта и доски
Items.onAdd = function (object) { // при добавлении
    if (object.type === 'Task' && object.up()) { // если задача на доске
        object.name = `[${object.up().up().up().name}, ${object.up().up().name}] ${object.name}`; // переименовываем
    }
};

// Скрипт: Берет первые три цифры из описания и крепит их к задаче числовым стикером
const yourNumSticker = Stickers.get('Число'); // напишите название своего стикера

Items.onUpdate = function(obj) { // при обновлении
    if (obj.type === 'Task') { // если задача
        let descDump = obj.getData().description; // берем наш дамп описания задачи
        if (descDump === undefined) {
            descDump = '';
        } // если дампа нет, то оставляем его пустым

        const currenDesc = Chat.getDescription(obj); // берем описание задачи

        if (descDump !== currenDesc) { // если разное с дампом
            const twoLetters = parseInt(currenDesc.slice(0, 6).replaceAll(/</?[^>]+(>|$)/gi, "")); // берем первые знаки как числа
            const numSticker = yourNumSticker; // берем стикер

            if (!Number.isNaN(twoLetters)) { // если нормальное число
                Stickers.pin(obj, numSticker, twoLetters); // крепим стикер
            }
        }

        obj.setData({
            description: currenDesc
        }); // сохраняем текущее описание в дамп на будущее
    }
};

// Скрипт: При клике на карточку задачи через Shift назначать себя исполнителем, а через Alt — еще удалить участников чата
Items.onClick = function (object) { // при клике
    if (object.type === 'Task') { // на задачу
        if (Events.isShiftPressed()) { // если зажата Shift
            Users.assignUser(Current.user, object); // назначаем себя на задачу
            Chat.addUser(object, Current.user); // добавляем себя в чат
            return false; // не открываем задачу
        }
        if (Events.isAltPressed()) { // если зажата Alt
            const listInChat = Chat.listUsers(object); // собираем участников чатов
            listInChat.forEach(user => Chat.removeUser(object, user)); // удаляем их из чата
            Users.assignUser(Current.user, object); // назначаем себя
            Chat.addUser(object, Current.user); // подписываем себя на чат
            return false; // не открываем задачу
        }
    }
    return true; // открываем задачу
};

// Скрипт: Прочитывает все чаты по Ctrl + Shift + Backspace
Events.onKeyUp = function (event) { // следим за клавиатурой
    if (Events.isShiftPressed() &&
        Events.isCtrlPressed() &&
        event.keyCode === 8) { // и Backspace
        const warn = confirm("Вы действительно хотите прочитать все чаты?"); // размещаем предупреждение
        if (warn) { // если все ок
            for (const project of Current.company.list()) // разбиваем компанию на проекты
                for (const boards of project.list()) // проекты на доски
                    for (const columns of boards.list()) // доски на колонки
                        for (const task of columns.list()) { // колонки на задачи
                            if (Chat.isUserInChat(task, Current.user)) { // берем только подписки
                                Chat.markRead(task, Current.user); // прочитываем задачи
                            }
                        }
        }
    }
};

// Скрипт: Открывает созданную задачу, если не зажата Alt. Если зажата — добавляет как обычно
Items.onAdd = function (task) { // при добавлении
    if (task.type === 'Task') { // задачи
        !Events.isAltPressed() ? Chat.open(task) : true; // при незажатой Alt открывает чат, иначе просто добавляет задачу
    }
};

// Скрипт: Создает несколько задач из списка
Items.onBeforeAdd = function (location, name) { // перед добавлением
    if (name.includes('n')) { // если есть перенос строк
        const tasks = name.split('n').reverse(); // режем строки на массивы по переносу обратным порядком
        tasks.forEach(name => location.add(name)); // добавляем задачи
        return false; // не добавляя изначальную
    }
};

// Скрипт: Добавляет второго исполнителя ко всем задачам первого исполнителя
// Для запуска скрипта нужно из-под администратора нажать Ctrl + Shift + / , в первом поле заполнить почту уже существующего исполнителя, во втором — нового исполнителя.
// Скрипт включает второго пользователя во все проекты существующего под ролью Сотрудник, не забудьте ее сменить.
// Работает только в браузере с возможностью открыть запрос (prompt).

Events.onKeyUp = function (event) { // следим за клавиатурой
    if (Events.isShiftPressed() &&
        Events.isCtrlPressed() &&
        Current.user.isAdmin &&
        event.keyCode === 191) { // если вызывает админ через Shift, Ctrl и /
        const assignedUserEmail = prompt('Введите email текущего исполнителя'); // запрашиваем первый email
        const userToAssignEmail = prompt('Введите email нового исполнителя'); // запрашиваем второй
        const assignedUser = Users.listAll().find(user => user.email === assignedUserEmail); // найдем текущего исполнителя
        const userToAssign = Users.listAll().find(user => user.email === userToAssignEmail); // найдем второго исполнителя
        // если email не найдены, то показываем ошибку
        if (assignedUser === undefined) { alert('Email текущего исполнителя не найден'); }
        if (userToAssign === undefined) { alert('Email нового исполнителя не найден'); }
        if (assignedUser !== undefined && userToAssign !== undefined) { // если все нормально
            for (const project of Current.company.list())
                for (const boards of project.list()) // проекты на доски
                    for (const columns of boards.list()) // доски на колонки
                        for (const task of columns.list()) { // колонки на задачи
                            if (Users.isUserAssigned(assignedUser, task)) { // если старый сотрудник подписан
                                const usersInTask = Users.getAssigned(task, true); // получим список уже подписанных сотрудников
                                const newUsersList = [...usersInTask, userToAssign]; // добавим в него нового
                                Users.assignUser(newUsersList, task); // применим этот список
                                Users.addToChat(userToAssign, task); // подпишем в чат
                                if (Users.isInProject(assignedUser, project)) { // в проекты, в которые добавлен текущий исполнитель
                                    Users.addToProject(userToAssign, project); // подпишем второго исполнителя
                                }
                            }
                        }
        }
    }
};

// Скрипт: Ограничивает количество задач в колонке
// В скрипте нужно заполнить два поля: id колонки в column и ограничение количества задач в ней
const column = '0da6cadd-7717-41cd-8dbe-6b571d15301a'; // id колонки
const count = 10; // максимальное количество задач в колонке

Items.onBeforeAdd = function (location, name) { // смотрим перед добавлением
    if (location.id === column) { // если колонка совпадает с нужной
        const columnCount = location.list().filter(x => x.isArchived() === false).length; // получаем количество неархивных задач
        if (columnCount >= count) { // если больше нашего количества
            alert('В колонке больше 10 задач!'); // выводим сообщение
            return false; // запрещаем добавление
        }
    } else {
        return true; // разрешаем
    }
};

Items.onMove = function (object, from, to) { // при перемещении задачи
    if (to.id === column) { // если колонка назначения совпадает с нужной
        const columnCount = to.list().filter(x => x.isArchived() === false).length; // получаем количество неархивных задач
        if (columnCount >= count) { // если больше нашего количества
            alert('В колонке больше 10 задач!'); // выводим сообщение
            return false; // запрещаем добавление
        }
    } else {
        return true; // разрешаем
    }
};

// Скрипт: Отписывает от всех задач доски через кнопку
// Работает только в браузере с возможностью открыть диалог подтверждения (confirm).

Current.onBoardChange = function (oldBoard, newBoard) {
    // При смене доски
    const boardList = Current.project.list(); // собираем все проекты
    boardList.forEach(function (board) {
        // Для каждой доски
        const btn = UI.button("Отписаться от всех задач"); // создаем кнопку
        board.ui.clear(); // предварительно очищаем, чтобы удалить предыдущий и не дублировать
        btn.onClick = function (e) {
            // В кнопке
            const warn = confirm("Вы действительно хотите отписаться от всех задач доски?"); // размещаем предупреждение
            if (warn) {
                // Если все ок
                for (const column of Current.board.list()) // для колонок в доске
                    for (const task of column.list()) {
                        // для задач в колонке
                        Chat.removeUser(task, Current.user); // отписываем пользователя
                    }
            }
        };
        board.ui.add(btn); // добавляем кнопку в интерфейс
    });
};

// Скрипт: Подписка на все чаты задач (при их создании)
// Скрипт работает только для администраторов компании, внутри скрипта нужно указать их список
// Не работает для подзадач.

// Нужно заполнить e-mail-ы администраторов в listOfEmails, которые будут подписываться на чаты
const listOfEmails = ["example1@example.com", "example2@example.com"]; // список email-ов для подписки
const findId = listOfEmails.map((email) => Users.get(email)); // переведем почты в id
const justAdmins = findId.filter((user) => user.isAdmin); // отфильтруем только администраторов

Items.onAdd = function (task) { // при добавлении
    if (task.type === "Task") { // задачи
        justAdmins.forEach((admin) => Chat.addUser(task, admin)); // подписываем на чат
    }
};

// Скрипт: Уведомление в чат о перемещении задачи
// Не работает для подзадач.

Items.onMove = function (task, from, to) { // при перемещении
    if (task.type === 'Task') { // задачи
        Chat.postMessage(task, '', `Сотрудник ${Current.user.name} переместил задачу из колонки «${from.name}» в колонку «${to.name}».`); // пишем сообщение в чат
    }
};

// Скрипт: Архивация задач при выполнении
// Может быть реализовано штатным функционалом YouGile: перейдите в Настройки доски > Основные и убедитесь, что галочка "Автоматически архивировать выполненные задачи" включена.

Items.onUpdate = function(obj) { // вызываем при обновлении
    if (obj.isCompleted() === true) { // если задача выполнена
        obj.setTaskCompleted(true); // то архивируем ее
    }
};

// Скрипт: Считает количество задач в колонке и печатает его в интерфейсе
// Пишет количество задач в колонках при отрисовке страницы и обновлении объектов.
// Вносить в него ничего не надо, можно в p.style поменять что-то на свой вкус.

Current.onBoardChange = function (oldBoard, newBoard) { // работаем при смене доски
    const listOfColumns = Current.board.list(); // получаем список колонок
    listOfColumns.forEach(function (i) { // перебираем каждую колонку
        if (!Items.get(i.id).isMirror() && !Items.get(i.id).isReport()) { // если колонка не зеркало и не сводка
            const column = Items.get(i.id); // получаем id колонки
            const numberTasks = column.list(); // берем общее количество задач в колонке
            const tasksIsNotArchived = numberTasks.filter(x => x.isArchived() === false); // забираем задачи не в архиве
            const p = UI.panel(); // создаем контейнер
            column.ui.clear(); // предварительно очищаем, чтобы удалить предыдущий и не дублировать
            column.ui.add(p);
            p.style = {
                margin: 'auto',
                width: '100px',
                padding: 'auto',
                color: '#572'
            }; // оформляем контейнер
            p.add(UI.text('Задач: ' + tasksIsNotArchived.length)); // пишем текст
        }
    });
};

Items.onUpdate = function (obj) { // вызываем функцию при обновлении, чтобы работало в реальном времени
    Current.onBoardChange();
};

// Скрипт: Запрашивает обязательный комментарий при перемещении задачи в другую колонку и печатает его в описание
// Работает только в браузере с возможностью открыть запрос (prompt).

Items.onMove = function(obj, from, to) {
    if (to.id === 'id колонки') { // если id колонки совпадает
        let comment = prompt('Введите комментарий'); // запрашиваем комментарий
        if (!comment) { // если комментария нет
            return false; // отказываем
        } else {
            Chat.postMessage(obj, '', `Задача перемещена пользователем ${Current.user.name}`); // пишем в чат, что переместили задачу
            Chat.setDescription(obj, Chat.getDescription(obj) + '<br> <p><b>Комментарий пользователя ' + Current.user.name + ': </b></p>' +
                '<p>' + comment + '</p>'); // пишем в описание комментарий
        }
        return true;
    }
};

// Скрипт: Назначение пользователя в определенной колонке
// Добавляет исполнителя в задачу, если ее переместить в колонку и у задачи нет исполнителя.
// Надо вписать id колонки и id пользователя.
// Для текущего пользователя (кто перемещает задачу) можно вместо id пользователя вписать Current.user.id без кавычек, это сделает его исполнителем.

Items.onMove = function(obj, from, to) {
    const task = Items.get(obj.id); // берем id таска
    const user = Users.get('id пользователя'); // берем id пользователя
    if (to.id === 'id колонки' && !Users.getAssigned(task)) { // если id колонки совпадает и в задаче нет исполнителя
        Users.assignUser(user, task); // назначаем пользователя
    }
};

// Скрипт: Скрывает задачу не из "белого списка" аккаунтов, если на нее навесить определенный стикер
// userRestrict — массив, в который вносятся email-ы, для которых задача скрывается

Items.isHidden = function(object) { // функция скрытия
    const stickerRestrict = Stickers.get('Ограничение'); // забираем нужный текстовый стикер
    const allowToSee = Stickers.getValue(object, stickerRestrict); // забираем состояние этого стикера
    const userRestrict = ['example1@example.com', 'example2@example.com']; // массив почт, для которых задача скроется
    function checkEmail(email) { // функция, которая возвращает совпадение запрещенного списка с текущим пользователем
        return email === Current.user.email;
    }
    const restrict = userRestrict.find(checkEmail); // ищет совпадение
    if (object.type === 'Task' && allowToSee === 'Запрет' && restrict !== undefined) { // если задача со стикером "Запрет" и текущий пользователь попадает в список
        return true; // скрываем
    }
    return false; // в остальных случаях должно быть открыто
};

// Скрипт: Сообщение в чат при удалении исполнителя
Stickers.onUnpin = function(task, sticker) { // при откреплении стикера
    if (sticker.name === 'User') { // если стикер — исполнитель
        const userID = Stickers.getValue(task, sticker); // берем id исполнителя
        const userName = Users.get(userID).name; // получаем его имя
        Chat.postMessage(task, '', `${userName} удален из исполнителей задачи`); // пишем в чат задачи
    }
    return true;
};

// Скрипт: Сообщение в чат при добавлении файла в описание задачи
// Ниже — единожды запускаемый процесс просмотра всех задач для фиксации дампа описания

for (const project of Current.company.list())
    for (const boards of project.list())
        for (const columns of boards.list())
            for (const task of columns.list()) {
                const dump = task.getData(); // читаем дамп
                const desc = Chat.getDescription(task); // и описание
                if (dump.description !== desc) { // если не совпадают
                    dump.description = desc;
                    task.setData(dump); // пишем дамп
                }
            }

// Дальше работаем по обновлению задач

Items.onUpdate = function(obj) { // при обновлении
    if (obj.id === Current.chat.id) { // фильтруем по открытой задаче (избегаем множества срабатываний)
        const dump = obj.getData(); // читаем дамп
        const currenDesc = Chat.getDescription(obj); // берем описание задачи
        const diff = findDiff(dump.description, currenDesc); // сравниваем и находим разницу
        if (diff.includes('rel="noopener noreferrer"')) { // если в разнице есть свойство вложения
            Chat.postMessage(obj, '', 'Файл добавлен в описание'); // пишем в чат
        }
        dump.description = currenDesc; // переписываем дамп в любом случае
        obj.setData(dump); // сохраняем текущее описание в дамп на будущее
    }
};

function findDiff(str1, str2) { // это функция, которая сравнивает две строки и выводит между ними разницу
    let diff = "";
    str2.split('').forEach(function(val, i) {
        if (val != str1.charAt(i))
            diff += val;
    });
    return diff;
}

// Скрипт: Сообщение в чат о прикреплении/изменении дедлайна
// Реагирует на создание и изменение дедлайна задачи, отписывая сообщение в чат задачи.
// Удаление дедлайна игнорирует

Stickers.onPin = function(task, sticker, value) { // при прикреплении стикера
    if (sticker.type === 'Deadline' && value !== undefined) { // если это дедлайн и у него есть значение
        Chat.postMessage(task, '', 'Прикреплен/изменен дедлайн'); // пишем сообщение в чат
    }
};

// Скрипт: Не дает добавить задачу, если есть с таким же названием в компании
// Не работает для подзадач.

Items.onBeforeAdd = function (location, name) { // перед добавлением
    if (location.type === 'Column') { // задачи
        for (const project of Current.company.list())
            for (const boards of project.list())
                for (const columns of boards.list())
                    for (const task of columns.list()) {
                        if (name == task.name) { // если название совпадает
                            Notifier.error('Задача с таким названием уже есть'); // показываем ошибку
                            return false; // запрещаем
                        }
                    }
        return true; // разрешаем, если ничего не запретилось
    }
};


// Скрипт: Отправка сообщения в чат при изменении чеклистов
Items.onUpdate = function(obj) { //при обновлении 
    if (obj.id === Current.chat.id) { //если объект — текущий чат
        const checkListsFact = Chat.getSubtasks(obj); // получаем чеклисты 
        const checkListsDump = obj.getData().checklists; // получаем дамп записи чеклистов
        if (JSON.stringify(checkListsFact) !== JSON.stringify(checkListsDump)) { //если дамп и чеклисты не совпадают
            if (checkListsDump === undefined) { //если дамп пуст
                obj.setData({
                    checklists: checkListsFact
                }); //записываем дамп
            } else if (checkListsFact.length !== checkListsDump.length) { //иначе сравниваем длину чеклста и дампа.
                // если разница есть → что-то делали со списком чеклистов
                obj.setData({
                    checklists: checkListsFact
                }); // записываем дамп
                Chat.postMessage(obj, Users.get(''), 'Изменен список чеклиста') // пишем в чат об изменении списка чеклиста
            } else { //иначе → если менялся не список чеклиста, то менялся пункт чеклиста
                obj.setData({
                    checklists: checkListsFact
                }); // записываем дамп
                Chat.postMessage(obj, Users.get(''), 'Изменен пункт чеклиста') // пишем в чат, что менялся пункт чеклиста
            }
        }
    }
};

// Скрипт: Копирует id задачи в буфер обмена по Ctrl + Alt + C
Events.onKeyDown = function (event) {
    if (
      Current.chat &&
      Current.chat.type === "Task" && //если задача
      Events.isCtrlPressed() && //и зажат контрол
      Events.isAltPressed() && //и альт
      event.keyCode === 67 // и C
    ) {
      const companyTail = Current.company.id.substring(Current.company.id.length - 12) // режеим хвостик
      const taskTail = Current.chat.id.substring(Current.chat.id.length - 12); //режеим хвостик
      App.copyToClipboard(
        `https://yougile.com/team/${companyTail}/#chat:${taskTail}`,
      ); // в буфер
      Notifier.success("Ссылка на тикет скопирована"); // уведомление
    }
  }

Мои скрипты

Множество задач из списка

Создать множество задачи из большого сообщения, которое вставляем в название страницы и нажимаем enter. В начале кода стоит удаление символов [ ] и даты. Вы перепишите функцию под свои «лишние» символы, которые надо удалить.

// Функция для удаления символов и дат в начале строки
function cleanTaskText(text) {
    // Удаляем символы - [ ] и дату в формате ДД.ММ.ГГГГ или с тире
    text = text.replace(/^s*-s*[s*]s*d{2}.d{2}.d{4}s*[-–—]?s*/, '');
    // Обрезаем лишние пробелы в начале и конце строки
    return text.trim();
}

// Отслеживаем добавление задачи
Items.onBeforeAdd = function (location, name) {
    if (name.includes('n')) { // Если есть переносы строк
        const tasks = name.split('n').reverse(); // Разделяем текст по строкам и переворачиваем массив
        tasks.forEach(taskName => {
            const cleanedTask = cleanTaskText(taskName); // Очищаем от символов и дат
            if (cleanedTask) {
                location.add(cleanedTask); // Добавляем очищенную задачу
            }
        });
        return false; // Не добавляем исходный текст
    }
};

Парсер числа из названия и установка стикера

Добавляет стикер суммы, если в начале есть сумма. Если стоит 10к, то конвертируется в 10 000. Стикер должен быть типа и называться «Сумма».

/**
 * Скрипт для автоматического добавления стикера "Сумма" к задачам.
 * Работает только для указанного проекта.
 * Извлекает сумму из названия задачи, независимо от ее позиции.
 * Добавляет стикер "Сумма", если он доступен в системе.
 * Поддерживает различные форматы записи суммы: рубли, тысячи, со знаком ₽.
 */

// ID целевого проекта
const TARGET_PROJECT_ID = '23c1c7b1-0402-4643-8026-100f887910dc';

// Получаем целевой проект
const targetProject = Items.get(TARGET_PROJECT_ID);

if (!targetProject) {
    console.error(`Проект с ID ${TARGET_PROJECT_ID} не найден.`);
    return;
}

// Глобальная переменная для хранения стикера "Сумма"
let sumSticker = null;

// Функция для инициализации стикера "Сумма"
function initSumSticker() {
    sumSticker = Stickers.get('Сумма');
    if (!sumSticker) {
        console.error("Стикер 'Сумма' не найден в системе.");
        return false;
    }
    console.log("Стикер 'Сумма' успешно инициализирован.");
    return true;
}

// Функция для проверки, принадлежит ли задача целевому проекту
function isTaskInTargetProject(task) {
    let currentItem = task;
    while (currentItem) {
        if (currentItem.id === TARGET_PROJECT_ID) {
            return true;
        }
        currentItem = currentItem.up();
    }
    return false;
}

// Функция для извлечения суммы из названия задачи
function extractSum(taskName) {
    const regex = /(d+(?:s*d+)*(?:,d+)?)s*(руб(?:лей)?|р|₽|k|к)?/i;
    const match = taskName.match(regex);

    if (match) {
        let sum = parseFloat(match[1].replace(/s/g, '').replace(',', '.'));
        const unit = match[2] ? match[2].toLowerCase() : '';

        if (unit === 'k' || unit === 'к') {
            sum *= 1000;
        }

        return Math.round(sum);
    }

    return null;
}

// Обработчик события создания задачи
Items.onAdd = function(task) {
    if (task && task.type === 'Task' && isTaskInTargetProject(task)) {
        console.log(`Создана новая задача: "${task.name}" в целевом проекте`);

        if (!sumSticker && !initSumSticker()) {
            console.log("Не удалось инициализировать стикер 'Сумма'. Стикер не будет добавлен.");
            return;
        }

        const taskName = task.name;
        const sum = extractSum(taskName);
        
        if (sum !== null) {
            setTimeout(() => {
                try {
                    Stickers.pin(task, sumSticker, sum.toString());
                    console.log(`Стикер 'Сумма' со значением ${sum} добавлен к задаче '${taskName}'`);
                } catch (error) {
                    console.error(`Ошибка при добавлении стикера: ${error.message}`);
                }
            }, 500);
        } else {
            console.log(`Сумма не найдена в названии задачи '${taskName}'`);
        }
    }
};

// Функция для добавления стикеров к существующим задачам
function addStickerToExistingTasks() {
    if (!sumSticker && !initSumSticker()) {
        console.log("Не удалось инициализировать стикер 'Сумма'. Обработка существующих задач прервана.");
        return;
    }

    console.log("Начало обработки существующих задач...");
    for (const board of targetProject.list()) {
        console.log(`Обработка доски: ${board.name}`);
        for (const column of board.list()) {
            for (const task of column.list()) {
                if (task && task.type === 'Task') {
                    const sum = extractSum(task.name);
                    if (sum !== null) {
                        try {
                            Stickers.pin(task, sumSticker, sum.toString());
                            console.log(`Стикер 'Сумма' со значением ${sum} добавлен к существующей задаче '${task.name}'`);
                        } catch (error) {
                            console.error(`Ошибка при добавлении стикера к задаче '${task.name}': ${error.message}`);
                        }
                    }
                }
            }
        }
    }
    console.log("Обработка существующих задач завершена.");
}

// Инициализация стикера при загрузке скрипта
if (initSumSticker()) {
    console.log(`Скрипт добавления стикера "Сумма" загружен и готов к работе для проекта с ID ${TARGET_PROJECT_ID}`);
} else {
    console.error("Скрипт не может работать без стикера 'Сумма'. Пожалуйста, создайте этот стикер в системе.");
}

// Раскомментируйте следующую строку, чтобы добавить стикеры к существующим задачам
// addStickerToExistingTasks();

Добавить стикер месяца к задаче

Добавляет стикер месяца к задаче. У меня стоит форматирование «Сентябрь 24». При создании задачи проверяет, какой месяц и добавляет стикер. По задумке, если будет январь 2025, то он создаст стикер Январь 25. Но надо дожить, чтобы проверить)

let buttonPanel;

function createButton(text, onClick) {
    const button = UI.button(text);
    button.style = {
        margin: '0 10px 0 0',
        padding: '10px 15px',
        fontSize: '14px',
        fontWeight: 'bold',
        backgroundColor: '#333',
        color: '#fff',  // Добавим белый цвет текста для контраста
        border: 'none',
        borderRadius: '6px',
        cursor: 'pointer'
    };
    button.onClick = onClick;
    return button;
}

function countSubscribedTasks(board) {
    return board.list().reduce((count, column) => 
        count + column.list().filter(task => Chat.isUserInChat(task, Current.user)).length, 0);
}

function countTasksAsExecutor(board) {
    const executorSticker = Stickers.get('User');
    return board.list().reduce((count, column) => 
        count + column.list().filter(task => 
            Stickers.getValue(task, executorSticker) === Current.user.id
        ).length, 0);
}

function unsubscribeFromAllTasks(board) {
    let unsubscribedCount = 0;
    board.list().forEach(column => {
        column.list().forEach(task => {
            if (Chat.isUserInChat(task, Current.user)) {
                Chat.removeUser(task, Current.user);
                unsubscribedCount++;
            }
        });
    });
    Notifier.success(`Вы отписались от ${unsubscribedCount} задач на этой доске.`);
}

function removeAsExecutorFromAllTasks(board) {
    const executorSticker = Stickers.get('User');
    let removedCount = 0;
    board.list().forEach(column => {
        column.list().forEach(task => {
            if (Stickers.getValue(task, executorSticker) === Current.user.id) {
                Stickers.unpin(task, executorSticker);
                removedCount++;
            }
        });
    });
    Notifier.success(`Вы удалены как исполнитель из ${removedCount} задач на этой доске.`);
}

function createOrUpdateButtonPanel(board) {
    if (!buttonPanel) {
        buttonPanel = UI.panel();
        buttonPanel.style = {
            display: 'flex',
            justifyContent: 'center',
            margin: '10px 0'
        };
    } else {
        buttonPanel.clear();
    }

    const unsubscribeButton = createButton(
        `Отписаться от всех задач доски`,
        () => {
            const count = countSubscribedTasks(board);
            if (confirm(`Вы уверены, что хотите отписаться от ${count} задач на этой доске?`)) {
                unsubscribeFromAllTasks(board);
            }
        }
    );

    const removeExecutorButton = createButton(
        `Удалить меня как исполнителя со всех задач`,
        () => {
            const count = countTasksAsExecutor(board);
            if (confirm(`Вы уверены, что хотите удалить себя как исполнителя из ${count} задач на этой доске?`)) {
                removeAsExecutorFromAllTasks(board);
            }
        }
    );

    buttonPanel.add(unsubscribeButton);
    buttonPanel.add(removeExecutorButton);

    return buttonPanel;
}

function addButtonPanelToBoard(board) {
    const panel = createOrUpdateButtonPanel(board);
    board.ui.add(panel);
}

Current.onBoardChange = function(oldBoard, newBoard) {
    if (newBoard) {
        addButtonPanelToBoard(newBoard);
    }
};

if (Current.board) {
    addButtonPanelToBoard(Current.board);
}

Добавить кнопки отписки и удаления себя от всех задач на доске

Добавляет две кнопки: отписать и удалить себя со всех задач на доске. Не на проекте, а на доске. При клике показывает окно, в котором указывает, из скольких задач отпишет или удалит.

let buttonPanel;

function createButton(text, onClick) {
    const button = UI.button(text);
    button.style = {
        margin: '0 10px 0 0',
        padding: '10px 15px',
        fontSize: '14px',
        fontWeight: 'bold',
        backgroundColor: '#333',
        color: '#fff',  // Добавим белый цвет текста для контраста
        border: 'none',
        borderRadius: '6px',
        cursor: 'pointer'
    };
    button.onClick = onClick;
    return button;
}

function countSubscribedTasks(board) {
    return board.list().reduce((count, column) => 
        count + column.list().filter(task => Chat.isUserInChat(task, Current.user)).length, 0);
}

function countTasksAsExecutor(board) {
    const executorSticker = Stickers.get('User');
    return board.list().reduce((count, column) => 
        count + column.list().filter(task => 
            Stickers.getValue(task, executorSticker) === Current.user.id
        ).length, 0);
}

function unsubscribeFromAllTasks(board) {
    let unsubscribedCount = 0;
    board.list().forEach(column => {
        column.list().forEach(task => {
            if (Chat.isUserInChat(task, Current.user)) {
                Chat.removeUser(task, Current.user);
                unsubscribedCount++;
            }
        });
    });
    Notifier.success(`Вы отписались от ${unsubscribedCount} задач на этой доске.`);
}

function removeAsExecutorFromAllTasks(board) {
    const executorSticker = Stickers.get('User');
    let removedCount = 0;
    board.list().forEach(column => {
        column.list().forEach(task => {
            if (Stickers.getValue(task, executorSticker) === Current.user.id) {
                Stickers.unpin(task, executorSticker);
                removedCount++;
            }
        });
    });
    Notifier.success(`Вы удалены как исполнитель из ${removedCount} задач на этой доске.`);
}

function createOrUpdateButtonPanel(board) {
    if (!buttonPanel) {
        buttonPanel = UI.panel();
        buttonPanel.style = {
            display: 'flex',
            justifyContent: 'center',
            margin: '10px 0'
        };
    } else {
        buttonPanel.clear();
    }

    const unsubscribeButton = createButton(
        `Отписаться от всех задач доски`,
        () => {
            const count = countSubscribedTasks(board);
            if (confirm(`Вы уверены, что хотите отписаться от ${count} задач на этой доске?`)) {
                unsubscribeFromAllTasks(board);
            }
        }
    );

    const removeExecutorButton = createButton(
        `Удалить меня как исполнителя со всех задач`,
        () => {
            const count = countTasksAsExecutor(board);
            if (confirm(`Вы уверены, что хотите удалить себя как исполнителя из ${count} задач на этой доске?`)) {
                removeAsExecutorFromAllTasks(board);
            }
        }
    );

    buttonPanel.add(unsubscribeButton);
    buttonPanel.add(removeExecutorButton);

    return buttonPanel;
}

function addButtonPanelToBoard(board) {
    const panel = createOrUpdateButtonPanel(board);
    board.ui.add(panel);
}

Current.onBoardChange = function(oldBoard, newBoard) {
    if (newBoard) {
        addButtonPanelToBoard(newBoard);
    }
};

if (Current.board) {
    addButtonPanelToBoard(Current.board);
}

История одного свитера

Я просто должен сохранить это в истории. Этот свитер как вторая кожа. На рукаве даже остались дырки от первых сигарет, которыми я случайно прожег его.

История одного свитера 11
История одного свитера 12
История одного свитера 13
История одного свитера 14
История одного свитера 15

P. S.
Мне кажется, меня в нем похоронят 😂 Предки, обновите потом фотки, по-братски.

#6 — Посетить город и увидеть роддом, где родился

Спустя много лет, я наконец посетил место, где был рожден. Хотел сделать это, когда был подростком.

Вот тут вот появился Данька Постнов, 28 лет назад.

#6 — Посетить город и увидеть роддом, где родился 16

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

#6 — Посетить город и увидеть роддом, где родился 17
#6 — Посетить город и увидеть роддом, где родился 18

В Рязани я успел почилить только в таком возрасте)

#6 — Посетить город и увидеть роддом, где родился 19

Заодно погулял по городу. Интересный город, я бы с радостью приехал туда ещё раз.

#64 — Прокатиться на гидроцикле в красивом месте

Когда мы с Лизой жили в Таиланде, мы купили 4-часовую экскурсию по островам на гидроцикле.

Я сразу втянулся, потому что часто ездил на байке, мотоцикле, велосипеде, а тут управление было примерно таким же.

На старте встретили парня, который с мамой арендовал гидроцикл. Из-за его желания покуражиться, они перевернулись три раза, чем оттягивали наше долгое путешествие.

После третьего раза организаторы посадили маму за руль. 😂

#64 — Прокатиться на гидроцикле в красивом месте 35

Доехали до первого острова и я предложил Лизе поменяться.

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

Она со страхом взялась за руль, резко дернула газ и с диким воплем мы неслись пару минут.

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

#64 — Прокатиться на гидроцикле в красивом месте 36

Когда я держался за ручки сзади, ногами было сложно упереться. Когда Лиза налетала на волну, моя задница подлетала вверх на метр, и я молил Бога, чтобы не улететь с этого гидроцикла.

#64 — Прокатиться на гидроцикле в красивом месте 37

Я буду не я, если не нырну в воду в рандомном месте 😂

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

Как делать скриншоты с высоким разрешением (4к, 8к) в Google Chrome и Firefox

Когда надо сделать скриншот хорошего качества в высоком разрешении, а у вас нет подходящего монитора. Что делать?

В этом нам поможет консоль разработчика в браузере.

Google Chrome

Видео 1:14

Firefox

Видео 1:00

Создание подписи на macbook

Создание подписи на macbook 38
Создание подписи на macbook 39

Далее нажимаем на «Камера», пишем на листочке подпись и подносим к камере. После удачного распознавания нажимаем «Готово».

Создание подписи на macbook 40

После она появится у вас в выборе подписей. Кликаете и она попадает на документ.

Создание подписи на macbook 41

Подпись должна быть синей. Я подобрал такой цвет. Кликайте по рамке и по цвету 3 слева и 3 сверху.

Создание подписи на macbook 42

Делаете нужный размер подписи, потягивая за углы, размещаете там, где нужно — готово. Документ подписан.

Не забывайте сохранять Ctrl + S.

Как сделать сжатие видео на Macos

В этой статье покажу, как сделать сжатие видео на macos без сторонних приложений.

Плюсы: делается в фоне, не ограничено по размеру, минимум действия для выполнения задачи.

Минусы: ноутбук начинает греться и шуметь, начинает подтормаживать на больших файлах

Порядок действий

1. Открываем Automator, можно через программы, можно через Spotlight

2. Выбираем «Быстрое действие»

Как сделать сжатие видео на Macos 43

3. Делаем следующие настройки. Изображение делаете то, которое понравится.

Как сделать сжатие видео на Macos 44

4. Находим пункт «Запустить shell-скрипт» и кликаем на нем дважды. Оно добавиться в окно справа.

Как сделать сжатие видео на Macos 45

5. Выбрать в shell /bin/bash

Как сделать сжатие видео на Macos 46

6. Справа выбрать «Как аргументы»

Как сделать сжатие видео на Macos 47

7. Удалить код, который сейчас и вставить код ниже

#!/bin/bash

for input_file in "$@"
do
  base_name=$(basename "$input_file" .mp4)
  temp_output_file="$HOME/Downloads/${base_name}_compressed — сжимается.mp4"
  final_output_file="$HOME/Downloads/${base_name}_compressed.mp4"

  # Начинаем сжатие во временный файл
  /opt/homebrew/bin/ffmpeg -i "$input_file" -vcodec libx264 -crf 17 -acodec copy "$temp_output_file"

  # Если сжатие прошло успешно, переименовываем файл
  if [ $? -eq 0 ]; then
    mv "$temp_output_file" "$final_output_file"

    # Выделяем созданный файл в Finder
    osascript <<EOF
      tell application "Finder"
        set theFile to POSIX file "$final_output_file" as alias
        select theFile
        activate
      end tell
EOF
  else
    echo "Ошибка при сжатии файла: $input_file"
    # Удаляем временный файл в случае ошибки
    rm -f "$temp_output_file"
  fi
done

# Открываем папку Downloads в Finder
open "$HOME/Downloads"

В итоге должно выглядеть так

Как сделать сжатие видео на Macos 48

8. Нажимаем CMS+S, он сохранится и попросить вас назвать быстрое действие. Называем «Сжать видео».

Как сделать сжатие видео на Macos 49

9. Выбираем видео-файл. Правой кнопкой мыши → быстрые действия → сжать видео

Как сделать сжатие видео на Macos 50

Видео добавится в эту же папку с примиской «_compressed».

Нюансы

Как менять качество видео?

Сжатием управляем через параметр -crf. Оптимально 17−20.

Как сделать сжатие видео на Macos 51

Как понять, что оно сжимается?

Когда нажмете «Сжать видео», в правом верхнем углу экрана появится шестеренка, это индикатор того, что скрипт выполняется. Проценты заполнятся не будут.

Как сделать сжатие видео на Macos 52

Как редактировать уже созданное быстрое действие?

  1. Откройте Finder,
  2. Нажмите СMD+Shift+G
  3. Вставьте ~/Library/Services/
  4. Нажмите Enter
Как сделать сжатие видео на Macos 53

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

Как сделать сжатие видео на Macos 54

Кликаете два раза и этот файл откроется в программе Automator, где вы сможете изменить его: подправить код или выбрать другую иконку.

Как экcпортировать все файлы из Figma

Дисклеймер

Нашел код на Reddit, поправил одну ошибку и скачал с его помощью все файлы. Доверился автору скрипта, за безопасность не отвечаю.

В статье скриншоты и видео записаны в Macos. Если у вас Windows, что-то может отличаться. Скриншоты для операций Windows я взял из гугла. Если они неактуальные, пишите в личку @daniilpostnov. Заменю.

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

Если что-то не получается, спрашивайте в личке или в комментариях. Занимает примерно 15 минут.

Проблема

Надо скачать все файлы из Figma, но нет кнопки экспорта. Есть только экспорт файлов вручную по одному.

Решение

Использовать код, который позволит автоматизировать скачивание.

⚠️ Это код не скачивает файлы из Drafts. Их надо перенести в проект. Если у вас бесплатный аккаунт, у вас не получится воспользоваться кодом. Нужно будет скачивать вручную. Советую оплатить Figma на один месяц, добавить проект, положить туда все файлы и выгрузить их. Я написал автору скрипта, возможно он допишет код, чтобы решить эту проблему.

Как оплатить фигму из России. Сам этими методами не пользовался, так как есть зарубежная карта. На свой страх и риск.

Как создать проект?

Порядок действий

  1. Если в Drafts есть файлы — добавить их в проект.
  2. Скачать архив с кодом по ссылке и распаковать его
  3. В файл вписать: логин, пароль, токен доступа, путь до папки, куда будут загружаться файлы
  4. Создать папку, куда будет выгружаться файлы
  5. Найти ID аккаунта в Figma
  6. Запустить две команды в терминале
  7. Ждать, пока все скачается
  8. Докачать вручную те файлы, что не скачаются автоматически

Скачиваем код

👉 Скачать архив с кодом

Архив нужно распаковать, зайти внутрь папки и найти файл .env

Заполняем файл .env

Этот файл скрытый. Если вы его не видите, надо включить показ скрытых файлов.

Как включить показ скрытых файлов

На Macos комбинация в папке — CMD + SHIFT +. (точка)

Как экcпортировать все файлы из Figma 55

На Windows — Вид → Скрытые элементы

Как экcпортировать все файлы из Figma 56

Кликаем по нему два раза, он отекроется в текстовом редакторе. В каком — зависит от вашей системы и наличия редакторов. Та файл выглядит внутри.

Как экcпортировать все файлы из Figma 57

Если он вдруг будет пустой — вставьте туда этот текст.

FIGMA_EMAIL="email@example.com"
FIGMA_PASSWORD="hunter2"
FIGMA_ACCESS_TOKEN="figd_abcdefghijklmnopqrstuvwxyz"
DOWNLOAD_PATH="/Users/anonymous/Downloads" # Абсолютный путь до папки, куда скачаются ваши файлы
WAIT_TIMEOUT=10000 # Время в милисекундах для ожидания между загрузками

Что вставлять в строки

  1. Логин
  2. Пароль. Если заходите через гугл, то зайдите на страницу входа, нажмите восстановить пароль и установите новый пароль. Ваш вход через гугл не затронет.
  3. Токен. Где его найти — ниже.
  4. Путь до папки. Создаем папку в загрузках и прописываем путь до неё. Как скопировать путь — ниже.
  5. Время между загрузками я не трогал. Оставил 10 000.

Где взять токен

Где взять токен

Как скопировать путь до папки

Как скопировать путь до папки

Вам нужно будет создать новую папку, куда будут скачиваться файлы. Я назвал свою FigmaExportResult

На Macos нажимаете Option и кликаете на папке правой кнопкой мыши. На выходе у меня получилось /Users/daniilpostnov/Downloads/FigmaExportResult. Это и надо вставить в строку DOWNLOAD_PATH.

Как экcпортировать все файлы из Figma 58

На Windows. Взял с гугла. Проверяйте на месте.

Как экcпортировать все файлы из Figma 59

Находим и копируем id аккаунта в фигме

Сохраните, пусть где-то рядом будет.

Авторизуйтесь в аккаунте Figma в браузере и из адресной строки возьмите ID вашего аккаунта. Он идет сразу что после files/team/

Как экcпортировать все файлы из Figma 60

Запустить две команды в терминале

Показано для Macos. На Windows ±тоже.

  1. Открываете Terminal
  2. Пишите cd и закидываете папку в окно Terminal
  3. Нажимаете Enter
  4. Вводите npm install, чтобы установить недостающие пакеты
  5. Пишем команду формирования списка файлов npm run get-team-files наш-id-аккаунта
  6. Пишем команду старта загрузки npm start

Ждем, пока все скачается

Начало загрузки выглядит так. Список файлов, где написан проект, файл и время, которое было потрачено на скачивание.

Как экcпортировать все файлы из Figma 61

Если все файлы будут скачаны успешно, в конце будет такой экран

Как экcпортировать все файлы из Figma 62

Докачать вручную те файлы, что не скачаются автоматически

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

На выходе получится структура вашего figma-аккаунта

Как экcпортировать все файлы из Figma 63

Мои результаты

Скачалось 400/425 файлов. 25 не скачались, выгрузил вручную.

Итоговая папка весит 17ГБ. Скачалось за 4.5 часа.

Подписывайтесь на канальчики, может там годнота попадется

Про дизайн и за жизнь:
@devpostnov

Про фриланс:
@nuance_freelance