Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions app/css/preferences.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,74 @@ body > div > :last-child {
margin-right: 32px;
}

.custom-messages-section > p {
margin: 0;
max-width: 420px;
}

.custom-messages-group + .custom-messages-group {
margin-top: 24px;
}

.custom-messages-group > h3 {
font-size: 13px;
font-weight: normal;
margin: 0 0 12px;
}

.custom-messages-table {
border-collapse: collapse;
margin-bottom: 12px;
width: 100%;
}

.custom-messages-table th,
.custom-messages-table td {
border-bottom: 1px solid var(--hr-color);
padding: 8px 12px 8px 0;
text-align: left;
vertical-align: middle;
}

body[dir=rtl] .custom-messages-table th,
body[dir=rtl] .custom-messages-table td {
padding: 8px 0 8px 12px;
text-align: right;
}

.custom-messages-table th {
font-weight: normal;
}

.custom-messages-table th:nth-child(2),
.custom-messages-table td:nth-child(2),
.custom-messages-table th:nth-child(3),
.custom-messages-table td:nth-child(3) {
white-space: nowrap;
width: 1%;
}

.custom-messages-table input[type="text"] {
box-sizing: border-box;
width: 100%;
}

.custom-messages-enabled {
align-items: center;
display: inline-flex;
justify-content: center;
min-width: 18px;
}

.custom-messages-enabled > label {
display: inline-block;
}

.custom-messages-add,
.custom-messages-delete {
white-space: nowrap;
}

.schedule {
display: grid;
font-size: 13px;
Expand Down
214 changes: 214 additions & 0 deletions app/custom-messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
const DEFAULT_LONG_BREAK_TITLE = 'Custom'

function normalizeMicrobreakIdeas (ideas = []) {
return ideas.map(idea => ({
data: typeof idea?.data === 'string' ? idea.data : '',
enabled: idea?.enabled !== false
}))
}

function normalizeBreakIdeas (ideas = []) {
return ideas.map(idea => {
const data = Array.isArray(idea?.data) ? idea.data : ['', '']
return {
data: [data[0] || '', data[1] || ''],
enabled: idea?.enabled !== false
}
})
}

function createTableCell (content) {
const cell = document.createElement('td')
if (content && typeof content.nodeType === 'number') {
cell.appendChild(content)
} else {
cell.textContent = content
}
return cell
}

export default class CustomMessages {
constructor ({ root, settings, saveSettings, translate, onHeightChange }) {
this.root = root
this.saveSettings = saveSettings
this.translate = translate
this.onHeightChange = onHeightChange
this.microbreakIdeas = normalizeMicrobreakIdeas(settings.microbreakIdeas)
this.breakIdeas = normalizeBreakIdeas(settings.breakIdeas)
}

async render () {
this.root.replaceChildren(
await this.#createSection({
titleKey: 'preferences.settings.microbreakMessages',
titleFallback: 'Mini break messages',
ideas: this.microbreakIdeas,
onAdd: async () => {
this.microbreakIdeas.push({ data: '', enabled: true })
this.saveSettings('microbreakIdeas', this.microbreakIdeas)
await this.render()
this.root.querySelector('[data-autofocus="microbreakIdeas"]')?.focus()
},
renderRow: async (idea, index) => this.#createMicrobreakRow(idea, index)
}),
await this.#createSection({
titleKey: 'preferences.settings.longBreakMessages',
titleFallback: 'Long break messages',
ideas: this.breakIdeas,
onAdd: async () => {
this.breakIdeas.push({ data: [DEFAULT_LONG_BREAK_TITLE, ''], enabled: true })
this.saveSettings('breakIdeas', this.breakIdeas)
await this.render()
this.root.querySelector('[data-autofocus="breakIdeas"]')?.focus()
},
renderRow: async (idea, index) => this.#createBreakRow(idea, index)
})
)

this.onHeightChange()
}

async #createSection ({ titleKey, titleFallback, ideas, onAdd, renderRow }) {
const section = document.createElement('section')
section.className = 'custom-messages-group'

const title = document.createElement('h3')
title.textContent = await this.translate(titleKey, titleFallback)

const table = document.createElement('table')
table.className = 'custom-messages-table'

const head = document.createElement('thead')
const headRow = document.createElement('tr')
headRow.append(
createTableCell(await this.translate('preferences.settings.customMessagesMessage', 'Message')),
createTableCell(await this.translate('preferences.settings.customMessagesEnabled', 'Enabled')),
createTableCell(await this.translate('preferences.settings.customMessagesDelete', 'Delete'))
)
head.appendChild(headRow)

const body = document.createElement('tbody')
for (const [index, idea] of ideas.entries()) {
body.appendChild(await renderRow(idea, index))
}

table.append(head, body)

const addButton = document.createElement('button')
addButton.type = 'button'
addButton.className = 'custom-messages-add'
addButton.textContent = await this.translate('preferences.settings.addNewMessage', 'Add new message')
addButton.onclick = onAdd

section.append(title, table, addButton)
return section
}

async #createMicrobreakRow (idea, index) {
const row = document.createElement('tr')

const messageInput = document.createElement('input')
messageInput.type = 'text'
messageInput.value = idea.data
if (idea.data === '') {
messageInput.dataset.autofocus = 'microbreakIdeas'
}
messageInput.oninput = () => {
this.microbreakIdeas[index].data = messageInput.value
this.saveSettings('microbreakIdeas', this.microbreakIdeas)
}

const enabledControl = await this.#createEnabledControl({
checked: idea.enabled,
scope: 'microbreakIdeas',
index,
onChange: checked => {
this.microbreakIdeas[index].enabled = checked
this.saveSettings('microbreakIdeas', this.microbreakIdeas)
}
})

const deleteButton = document.createElement('button')
deleteButton.type = 'button'
deleteButton.className = 'custom-messages-delete'
deleteButton.textContent = await this.translate('preferences.settings.customMessagesDelete', 'Delete')
deleteButton.onclick = async () => {
this.microbreakIdeas.splice(index, 1)
this.saveSettings('microbreakIdeas', this.microbreakIdeas)
await this.render()
}

row.append(
createTableCell(messageInput),
createTableCell(enabledControl),
createTableCell(deleteButton)
)

return row
}

async #createBreakRow (idea, index) {
const row = document.createElement('tr')

const messageInput = document.createElement('input')
messageInput.type = 'text'
messageInput.value = idea.data[1]
if (idea.data[1] === '') {
messageInput.dataset.autofocus = 'breakIdeas'
}
messageInput.oninput = () => {
this.breakIdeas[index].data[1] = messageInput.value
this.saveSettings('breakIdeas', this.breakIdeas)
}

const enabledControl = await this.#createEnabledControl({
checked: idea.enabled,
scope: 'breakIdeas',
index,
onChange: checked => {
this.breakIdeas[index].enabled = checked
this.saveSettings('breakIdeas', this.breakIdeas)
}
})

const deleteButton = document.createElement('button')
deleteButton.type = 'button'
deleteButton.className = 'custom-messages-delete'
deleteButton.textContent = await this.translate('preferences.settings.customMessagesDelete', 'Delete')
deleteButton.onclick = async () => {
this.breakIdeas.splice(index, 1)
this.saveSettings('breakIdeas', this.breakIdeas)
await this.render()
}

row.append(
createTableCell(messageInput),
createTableCell(enabledControl),
createTableCell(deleteButton)
)

return row
}

async #createEnabledControl ({ checked, scope, index, onChange }) {
const wrapper = document.createElement('div')
wrapper.className = 'custom-messages-enabled'

const checkboxId = `${scope}-enabled-${index}`
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.id = checkboxId
checkbox.dataset.customMessageControl = 'true'
checkbox.checked = checked
checkbox.onchange = () => {
onChange(checkbox.checked)
}

const label = document.createElement('label')
label.htmlFor = checkboxId
label.title = await this.translate('preferences.settings.customMessagesEnabled', 'Enabled')

wrapper.append(checkbox, label)
return wrapper
}
}
11 changes: 10 additions & 1 deletion app/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@
"window": "Window",
"fullscreen": "Full screen",
"showIdeas": "Show exercise tips during breaks",
"useCustomMessages": "Use custom messages",
"customMessages": "Custom Messages",
"customMessagesInfo": "Edit the existing custom mini break and long break messages stored in the configuration.",
"microbreakMessages": "Mini break messages",
"longBreakMessages": "Long break messages",
"customMessagesMessage": "Message",
"customMessagesEnabled": "Enabled",
"customMessagesDelete": "Delete",
"addNewMessage": "Add new message",
"allScreens": "Shows breaks on all monitors",
"monitorIdleTime": "Monitor system idle time (breaks are paused if system is idle).",
"monitorDnd": "Show breaks even in Do Not Disturb mode",
Expand Down Expand Up @@ -517,4 +526,4 @@
"text": "Try the 5-4-3-2-1 Grounding Technique: Identify 5 things you can see, 4 things you can touch, 3 things you can hear, 2 things you can smell, and 1 thing you can taste."
}
}
}
}
4 changes: 4 additions & 0 deletions app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,10 @@ ipcMain.on('save-setting', function (event, key, value) {

settings.set(key, value)

if (['microbreakIdeas', 'breakIdeas', 'useIdeasFromSettings'].includes(key)) {
loadIdeas()
}

updateTray()
})

Expand Down
22 changes: 20 additions & 2 deletions app/preferences-renderer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import VersionChecker from './utils/versionChecker.js'
import { setSameWidths } from './utils/sameWidths.js'
import HtmlTranslate from './utils/htmlTranslate.js'
import CustomMessages from './custom-messages.js'

import './platform.js'

Expand Down Expand Up @@ -31,6 +32,22 @@ window.onload = async (e) => {
setWindowHeight()
setTimeout(() => { eventsAttached = true }, 500)

const customMessages = new CustomMessages({
root: document.querySelector('#customMessages'),
settings,
saveSettings: (key, value) => window.settings.saveSettings(key, value),
translate: async (key, fallback) => {
const translated = await window.i18next.t(key)
return translated === key ? fallback : translated
},
onHeightChange: () => {
setSameWidths()
setWindowHeight()
}
})

await customMessages.render()

if (settings.customPreferencesMessage) {
const customMessageDiv = document.createElement('div')
customMessageDiv.className = 'custom-message'
Expand Down Expand Up @@ -100,6 +117,7 @@ window.onload = async (e) => {

window.stretchly.onTranslate(async () => {
new HtmlTranslate(document).translate()
await customMessages.render()
document.querySelectorAll('input[type="range"]').forEach(async range => {
const settings = await window.settings.currentSettings()
const divisor = range.dataset.divisor
Expand Down Expand Up @@ -178,7 +196,7 @@ window.onload = async (e) => {
}
})

document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
document.querySelectorAll('input[type="checkbox"]:not([data-custom-message-control])').forEach(checkbox => {
const isNegative = checkbox.classList.contains('negative')
checkbox.checked = isNegative ? !settings[checkbox.value] : settings[checkbox.value]
if (!eventsAttached) {
Expand Down Expand Up @@ -265,7 +283,7 @@ window.onload = async (e) => {
}
})

document.querySelector('.settings > div > button').onclick = (event) => {
document.querySelector('.settings > div:last-child > button').onclick = (event) => {
window.stretchly.restoreDefaults()
}

Expand Down
Loading