biome format --write
This commit is contained in:
parent
8372348148
commit
9262e803ec
415 changed files with 54076 additions and 17419 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,9 @@
|
|||
import { kebabCase } from 'lodash'
|
||||
|
||||
const propsToNative = props => Object.keys(props).reduce((acc, cur) => {
|
||||
acc[kebabCase(cur)] = props[cur]
|
||||
return acc
|
||||
}, {})
|
||||
const propsToNative = (props) =>
|
||||
Object.keys(props).reduce((acc, cur) => {
|
||||
acc[kebabCase(cur)] = props[cur]
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
export { propsToNative }
|
||||
|
|
|
|||
|
|
@ -1,40 +1,60 @@
|
|||
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
|
||||
import apiService, {
|
||||
getMastodonSocketURI,
|
||||
ProcessedWS,
|
||||
} from '../api/api.service.js'
|
||||
import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js'
|
||||
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
||||
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
|
||||
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
|
||||
import bookmarkFoldersFetcher from '../../services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js'
|
||||
|
||||
const backendInteractorService = credentials => ({
|
||||
startFetchingTimeline ({ timeline, store, userId = false, listId = false, statusId = false, bookmarkFolderId = false, tag }) {
|
||||
return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, statusId, bookmarkFolderId, tag })
|
||||
const backendInteractorService = (credentials) => ({
|
||||
startFetchingTimeline({
|
||||
timeline,
|
||||
store,
|
||||
userId = false,
|
||||
listId = false,
|
||||
statusId = false,
|
||||
bookmarkFolderId = false,
|
||||
tag,
|
||||
}) {
|
||||
return timelineFetcher.startFetching({
|
||||
timeline,
|
||||
store,
|
||||
credentials,
|
||||
userId,
|
||||
listId,
|
||||
statusId,
|
||||
bookmarkFolderId,
|
||||
tag,
|
||||
})
|
||||
},
|
||||
|
||||
fetchTimeline (args) {
|
||||
fetchTimeline(args) {
|
||||
return timelineFetcher.fetchAndUpdate({ ...args, credentials })
|
||||
},
|
||||
|
||||
startFetchingNotifications ({ store }) {
|
||||
startFetchingNotifications({ store }) {
|
||||
return notificationsFetcher.startFetching({ store, credentials })
|
||||
},
|
||||
|
||||
fetchNotifications (args) {
|
||||
fetchNotifications(args) {
|
||||
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
|
||||
},
|
||||
|
||||
startFetchingFollowRequests ({ store }) {
|
||||
startFetchingFollowRequests({ store }) {
|
||||
return followRequestFetcher.startFetching({ store, credentials })
|
||||
},
|
||||
|
||||
startFetchingLists ({ store }) {
|
||||
startFetchingLists({ store }) {
|
||||
return listsFetcher.startFetching({ store, credentials })
|
||||
},
|
||||
|
||||
startFetchingBookmarkFolders ({ store }) {
|
||||
startFetchingBookmarkFolders({ store }) {
|
||||
return bookmarkFoldersFetcher.startFetching({ store, credentials })
|
||||
},
|
||||
|
||||
startUserSocket ({ store }) {
|
||||
startUserSocket({ store }) {
|
||||
const serv = store.rootState.instance.server.replace('http', 'ws')
|
||||
const url = getMastodonSocketURI({}, serv)
|
||||
return ProcessedWS({ url, id: 'Unified', credentials })
|
||||
|
|
@ -43,11 +63,11 @@ const backendInteractorService = credentials => ({
|
|||
...Object.entries(apiService).reduce((acc, [key, func]) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: (args) => func({ credentials, ...args })
|
||||
[key]: (args) => func({ credentials, ...args }),
|
||||
}
|
||||
}, {}),
|
||||
|
||||
verifyCredentials: apiService.verifyCredentials
|
||||
verifyCredentials: apiService.verifyCredentials,
|
||||
})
|
||||
|
||||
export default backendInteractorService
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ import { promiseInterval } from '../promise_interval/promise_interval.js'
|
|||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
|
||||
const fetchAndUpdate = ({ credentials }) => {
|
||||
return apiService.fetchBookmarkFolders({ credentials })
|
||||
.then(bookmarkFolders => {
|
||||
useBookmarkFoldersStore().setBookmarkFolders(bookmarkFolders)
|
||||
}, () => {})
|
||||
return apiService
|
||||
.fetchBookmarkFolders({ credentials })
|
||||
.then(
|
||||
(bookmarkFolders) => {
|
||||
useBookmarkFoldersStore().setBookmarkFolders(bookmarkFolders)
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
|
|
@ -17,7 +21,7 @@ const startFetching = ({ credentials, store }) => {
|
|||
}
|
||||
|
||||
const bookmarkFoldersFetcher = {
|
||||
startFetching
|
||||
startFetching,
|
||||
}
|
||||
|
||||
export default bookmarkFoldersFetcher
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const empty = (chatId) => {
|
|||
lastSeenMessageId: '0',
|
||||
chatId,
|
||||
minId: undefined,
|
||||
maxId: undefined
|
||||
maxId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +25,9 @@ const clear = (storage) => {
|
|||
}
|
||||
}
|
||||
|
||||
storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id))
|
||||
storage.messages = storage.messages.filter((m) =>
|
||||
failedMessageIds.includes(m.id),
|
||||
)
|
||||
storage.newMessageCount = 0
|
||||
storage.lastSeenMessageId = '0'
|
||||
storage.minId = undefined
|
||||
|
|
@ -33,8 +35,10 @@ const clear = (storage) => {
|
|||
}
|
||||
|
||||
const deleteMessage = (storage, messageId) => {
|
||||
if (!storage) { return }
|
||||
storage.messages = storage.messages.filter(m => m.id !== messageId)
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
storage.messages = storage.messages.filter((m) => m.id !== messageId)
|
||||
delete storage.idIndex[messageId]
|
||||
|
||||
if (storage.maxId === messageId) {
|
||||
|
|
@ -65,14 +69,20 @@ const cullOlderMessages = (storage) => {
|
|||
}
|
||||
|
||||
const handleMessageError = (storage, fakeId, isRetry) => {
|
||||
if (!storage) { return }
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
const fakeMessage = storage.idIndex[fakeId]
|
||||
if (fakeMessage) {
|
||||
fakeMessage.error = true
|
||||
fakeMessage.pending = false
|
||||
if (!isRetry) {
|
||||
// Ensure the failed message doesn't stay at the bottom of the list.
|
||||
const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0]
|
||||
const lastPersistedMessage = _.orderBy(
|
||||
storage.messages,
|
||||
['pending', 'id'],
|
||||
['asc', 'desc'],
|
||||
)[0]
|
||||
if (lastPersistedMessage) {
|
||||
const oldId = fakeMessage.id
|
||||
fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}`
|
||||
|
|
@ -84,12 +94,16 @@ const handleMessageError = (storage, fakeId, isRetry) => {
|
|||
}
|
||||
|
||||
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
||||
if (!storage) { return }
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < newMessages.length; i++) {
|
||||
const message = newMessages[i]
|
||||
|
||||
// sanity check
|
||||
if (message.chat_id !== storage.chatId) { return }
|
||||
if (message.chat_id !== storage.chatId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (message.fakeId) {
|
||||
const fakeMessage = storage.idIndex[message.fakeId]
|
||||
|
|
@ -98,7 +112,9 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
|||
// make sure to remove the older duplicate message.
|
||||
if (storage.idIndex[message.id]) {
|
||||
delete storage.idIndex[message.id]
|
||||
storage.messages = storage.messages.filter(msg => msg.id !== message.id)
|
||||
storage.messages = storage.messages.filter(
|
||||
(msg) => msg.id !== message.id,
|
||||
)
|
||||
}
|
||||
Object.assign(fakeMessage, message, { error: false })
|
||||
delete fakeMessage.fakeId
|
||||
|
|
@ -136,17 +152,25 @@ const isConfirmation = (storage, message) => {
|
|||
}
|
||||
|
||||
const resetNewMessageCount = (storage) => {
|
||||
if (!storage) { return }
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
storage.newMessageCount = 0
|
||||
storage.lastSeenMessageId = storage.maxId
|
||||
}
|
||||
|
||||
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
|
||||
const getView = (storage) => {
|
||||
if (!storage) { return [] }
|
||||
if (!storage) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result = []
|
||||
const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
|
||||
const messages = _.orderBy(
|
||||
storage.messages,
|
||||
['pending', 'id'],
|
||||
['asc', 'asc'],
|
||||
)
|
||||
const firstMessage = messages[0]
|
||||
let previousMessage = messages[messages.length - 1]
|
||||
let currentMessageChainId
|
||||
|
|
@ -157,7 +181,7 @@ const getView = (storage) => {
|
|||
result.push({
|
||||
type: 'date',
|
||||
date,
|
||||
id: date.getTime().toString()
|
||||
id: date.getTime().toString(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +199,7 @@ const getView = (storage) => {
|
|||
result.push({
|
||||
type: 'date',
|
||||
date,
|
||||
id: date.getTime().toString()
|
||||
id: date.getTime().toString(),
|
||||
})
|
||||
|
||||
previousMessage.isTail = true
|
||||
|
|
@ -188,7 +212,7 @@ const getView = (storage) => {
|
|||
data: message,
|
||||
date,
|
||||
id: message.id,
|
||||
messageChainId: currentMessageChainId
|
||||
messageChainId: currentMessageChainId,
|
||||
}
|
||||
|
||||
// end a message chian
|
||||
|
|
@ -198,7 +222,12 @@ const getView = (storage) => {
|
|||
}
|
||||
|
||||
// start a new message chain
|
||||
if ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) {
|
||||
if (
|
||||
(previousMessage &&
|
||||
previousMessage.data &&
|
||||
previousMessage.data.account_id) !== message.account_id ||
|
||||
afterDate
|
||||
) {
|
||||
currentMessageChainId = _.uniqueId()
|
||||
object.isHead = true
|
||||
object.messageChainId = currentMessageChainId
|
||||
|
|
@ -220,7 +249,7 @@ const ChatService = {
|
|||
cullOlderMessages,
|
||||
resetNewMessageCount,
|
||||
clear,
|
||||
handleMessageError
|
||||
handleMessageError,
|
||||
}
|
||||
|
||||
export default ChatService
|
||||
|
|
|
|||
|
|
@ -2,24 +2,35 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n
|
|||
|
||||
export const maybeShowChatNotification = (store, chat) => {
|
||||
if (!chat.lastMessage) return
|
||||
if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
|
||||
if (store.rootState.users.currentUser.id === chat.lastMessage.account_id) return
|
||||
if (store.rootState.chats.currentChatId === chat.id && !document.hidden)
|
||||
return
|
||||
if (store.rootState.users.currentUser.id === chat.lastMessage.account_id)
|
||||
return
|
||||
|
||||
const opts = {
|
||||
tag: chat.lastMessage.id,
|
||||
title: chat.account.name,
|
||||
icon: chat.account.profile_image_url,
|
||||
body: chat.lastMessage.content
|
||||
body: chat.lastMessage.content,
|
||||
}
|
||||
|
||||
if (chat.lastMessage.attachment && chat.lastMessage.attachment.type === 'image') {
|
||||
if (
|
||||
chat.lastMessage.attachment &&
|
||||
chat.lastMessage.attachment.type === 'image'
|
||||
) {
|
||||
opts.image = chat.lastMessage.attachment.preview_url
|
||||
}
|
||||
|
||||
showDesktopNotification(store.rootState, opts)
|
||||
}
|
||||
|
||||
export const buildFakeMessage = ({ content, chatId, attachments, userId, idempotencyKey }) => {
|
||||
export const buildFakeMessage = ({
|
||||
content,
|
||||
chatId,
|
||||
attachments,
|
||||
userId,
|
||||
idempotencyKey,
|
||||
}) => {
|
||||
const fakeMessage = {
|
||||
content,
|
||||
chat_id: chatId,
|
||||
|
|
@ -30,7 +41,7 @@ export const buildFakeMessage = ({ content, chatId, attachments, userId, idempot
|
|||
idempotency_key: idempotencyKey,
|
||||
emojis: [],
|
||||
pending: true,
|
||||
isNormalized: true
|
||||
isNormalized: true,
|
||||
}
|
||||
|
||||
if (attachments[0]) {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ export const rgb2hex = (r, g, b) => {
|
|||
return r
|
||||
}
|
||||
if (typeof r === 'object') {
|
||||
({ r, g, b } = r)
|
||||
;({ r, g, b } = r)
|
||||
}
|
||||
[r, g, b] = [r, g, b].map(val => {
|
||||
;[r, g, b] = [r, g, b].map((val) => {
|
||||
val = Math.ceil(val)
|
||||
val = val < 0 ? 0 : val
|
||||
val = val > 255 ? 255 : val
|
||||
|
|
@ -137,9 +137,9 @@ export const alphaBlend = (fg, fga, bg) => {
|
|||
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
|
||||
// for opaque bg and transparent fg
|
||||
return {
|
||||
r: (fg.r * fga + bg.r * (1 - fga)),
|
||||
g: (fg.g * fga + bg.g * (1 - fga)),
|
||||
b: (fg.b * fga + bg.b * (1 - fga))
|
||||
r: fg.r * fga + bg.r * (1 - fga),
|
||||
g: fg.g * fga + bg.g * (1 - fga),
|
||||
b: fg.b * fga + bg.b * (1 - fga),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,15 +149,16 @@ export const alphaBlend = (fg, fga, bg) => {
|
|||
* @param {Object} bedrock - layer at the very bottom
|
||||
* @param {[Object, Number]} layers[] - layers between text and bedrock
|
||||
*/
|
||||
export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color, opacity]) => {
|
||||
return alphaBlend(color, opacity, acc)
|
||||
}, bedrock)
|
||||
export const alphaBlendLayers = (bedrock, layers) =>
|
||||
layers.reduce((acc, [color, opacity]) => {
|
||||
return alphaBlend(color, opacity, acc)
|
||||
}, bedrock)
|
||||
|
||||
export const invert = (rgb) => {
|
||||
return {
|
||||
r: 255 - rgb.r,
|
||||
g: 255 - rgb.g,
|
||||
b: 255 - rgb.b
|
||||
b: 255 - rgb.b,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +175,7 @@ export const hex2rgb = (hex) => {
|
|||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
|
@ -190,7 +191,7 @@ export const mixrgb = (a, b) => {
|
|||
return {
|
||||
r: (a.r + b.r) / 2,
|
||||
g: (a.g + b.g) / 2,
|
||||
b: (a.b + b.b) / 2
|
||||
b: (a.b + b.b) / 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +206,7 @@ export const rgba2css = function (rgba) {
|
|||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1
|
||||
a: 1,
|
||||
}
|
||||
|
||||
if (rgba !== null) {
|
||||
|
|
|
|||
|
|
@ -12,26 +12,30 @@ export const wordAtPosition = (str, pos) => {
|
|||
}
|
||||
|
||||
export const addPositionToWords = (words) => {
|
||||
return reduce(words, (result, word) => {
|
||||
const data = {
|
||||
word,
|
||||
start: 0,
|
||||
end: word.length
|
||||
}
|
||||
return reduce(
|
||||
words,
|
||||
(result, word) => {
|
||||
const data = {
|
||||
word,
|
||||
start: 0,
|
||||
end: word.length,
|
||||
}
|
||||
|
||||
if (result.length > 0) {
|
||||
const previous = result.pop()
|
||||
if (result.length > 0) {
|
||||
const previous = result.pop()
|
||||
|
||||
data.start += previous.end
|
||||
data.end += previous.end
|
||||
data.start += previous.end
|
||||
data.end += previous.end
|
||||
|
||||
result.push(previous)
|
||||
}
|
||||
result.push(previous)
|
||||
}
|
||||
|
||||
result.push(data)
|
||||
result.push(data)
|
||||
|
||||
return result
|
||||
}, [])
|
||||
return result
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
export const splitByWhitespaceBoundary = (str) => {
|
||||
|
|
@ -64,7 +68,7 @@ const completion = {
|
|||
wordAtPosition,
|
||||
addPositionToWords,
|
||||
splitByWhitespaceBoundary,
|
||||
replaceWord
|
||||
replaceWord,
|
||||
}
|
||||
|
||||
export default completion
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import isFunction from 'lodash/isFunction'
|
||||
|
||||
const getComponentOptions = (Component) => (isFunction(Component)) ? Component.options : Component
|
||||
const getComponentOptions = (Component) =>
|
||||
isFunction(Component) ? Component.options : Component
|
||||
|
||||
const getComponentProps = (Component) => getComponentOptions(Component).props
|
||||
|
||||
export {
|
||||
getComponentOptions,
|
||||
getComponentProps
|
||||
}
|
||||
export { getComponentOptions, getComponentProps }
|
||||
|
|
|
|||
|
|
@ -47,17 +47,23 @@ export const relativeTimeShort = (date, nowThreshold = 1) => {
|
|||
|
||||
export const unitToSeconds = (unit, amount) => {
|
||||
switch (unit) {
|
||||
case 'minutes': return 0.001 * amount * MINUTE
|
||||
case 'hours': return 0.001 * amount * HOUR
|
||||
case 'days': return 0.001 * amount * DAY
|
||||
case 'minutes':
|
||||
return 0.001 * amount * MINUTE
|
||||
case 'hours':
|
||||
return 0.001 * amount * HOUR
|
||||
case 'days':
|
||||
return 0.001 * amount * DAY
|
||||
}
|
||||
}
|
||||
|
||||
export const secondsToUnit = (unit, amount) => {
|
||||
switch (unit) {
|
||||
case 'minutes': return (1000 * amount) / MINUTE
|
||||
case 'hours': return (1000 * amount) / HOUR
|
||||
case 'days': return (1000 * amount) / DAY
|
||||
case 'minutes':
|
||||
return (1000 * amount) / MINUTE
|
||||
case 'hours':
|
||||
return (1000 * amount) / HOUR
|
||||
case 'days':
|
||||
return (1000 * amount) / DAY
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,14 +72,15 @@ export const isSameYear = (a, b) => {
|
|||
}
|
||||
|
||||
export const isSameMonth = (a, b) => {
|
||||
return a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth()
|
||||
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()
|
||||
}
|
||||
|
||||
export const isSameDay = (a, b) => {
|
||||
return a.getFullYear() === b.getFullYear() &&
|
||||
return (
|
||||
a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth() &&
|
||||
a.getDate() === b.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
export const durationStrToMs = (str) => {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,27 @@
|
|||
import {
|
||||
showDesktopNotification as swDesktopNotification,
|
||||
closeDesktopNotification as swCloseDesktopNotification,
|
||||
isSWSupported
|
||||
isSWSupported,
|
||||
} from '../sw/sw.js'
|
||||
const state = { failCreateNotif: false }
|
||||
|
||||
export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
|
||||
if (!('Notification' in window && window.Notification.permission === 'granted')) return
|
||||
if (rootState.notifications.desktopNotificationSilence) { return }
|
||||
if (
|
||||
!('Notification' in window && window.Notification.permission === 'granted')
|
||||
)
|
||||
return
|
||||
if (rootState.notifications.desktopNotificationSilence) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isSWSupported()) {
|
||||
swDesktopNotification(desktopNotificationOpts)
|
||||
} else if (!state.failCreateNotif) {
|
||||
try {
|
||||
const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
|
||||
const desktopNotification = new window.Notification(
|
||||
desktopNotificationOpts.title,
|
||||
desktopNotificationOpts,
|
||||
)
|
||||
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
|
||||
} catch {
|
||||
state.failCreateNotif = true
|
||||
|
|
@ -22,7 +30,10 @@ export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
|
|||
}
|
||||
|
||||
export const closeDesktopNotification = (rootState, { id }) => {
|
||||
if (!('Notification' in window && window.Notification.permission === 'granted')) return
|
||||
if (
|
||||
!('Notification' in window && window.Notification.permission === 'granted')
|
||||
)
|
||||
return
|
||||
|
||||
if (isSWSupported()) {
|
||||
swCloseDesktopNotification({ id })
|
||||
|
|
@ -30,7 +41,10 @@ export const closeDesktopNotification = (rootState, { id }) => {
|
|||
}
|
||||
|
||||
export const closeAllDesktopNotifications = () => {
|
||||
if (!('Notification' in window && window.Notification.permission === 'granted')) return
|
||||
if (
|
||||
!('Notification' in window && window.Notification.permission === 'granted')
|
||||
)
|
||||
return
|
||||
|
||||
if (isSWSupported()) {
|
||||
swCloseDesktopNotification({})
|
||||
|
|
|
|||
|
|
@ -21,16 +21,25 @@ const qvitterStatusType = (status) => {
|
|||
return 'retweet'
|
||||
}
|
||||
|
||||
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
|
||||
(typeof status.text === 'string' && status.text.match(/favorited/))) {
|
||||
if (
|
||||
(typeof status.uri === 'string' &&
|
||||
status.uri.match(/(fave|objectType=Favourite)/)) ||
|
||||
(typeof status.text === 'string' && status.text.match(/favorited/))
|
||||
) {
|
||||
return 'favorite'
|
||||
}
|
||||
|
||||
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
|
||||
if (
|
||||
status.text.match(/deleted notice {{tag/) ||
|
||||
status.qvitter_delete_notice
|
||||
) {
|
||||
return 'deletion'
|
||||
}
|
||||
|
||||
if (status.text.match(/started following/) || status.activity_type === 'follow') {
|
||||
if (
|
||||
status.text.match(/started following/) ||
|
||||
status.activity_type === 'follow'
|
||||
) {
|
||||
return 'follow'
|
||||
}
|
||||
|
||||
|
|
@ -69,16 +78,16 @@ export const parseUser = (data) => {
|
|||
output.description_html = data.note
|
||||
|
||||
output.fields = data.fields
|
||||
output.fields_html = data.fields.map(field => {
|
||||
output.fields_html = data.fields.map((field) => {
|
||||
return {
|
||||
name: escape(field.name),
|
||||
value: field.value
|
||||
value: field.value,
|
||||
}
|
||||
})
|
||||
output.fields_text = data.fields.map(field => {
|
||||
output.fields_text = data.fields.map((field) => {
|
||||
return {
|
||||
name: unescape(field.name.replace(/<[^>]*>/g, '')),
|
||||
value: unescape(field.value.replace(/<[^>]*>/g, ''))
|
||||
value: unescape(field.value.replace(/<[^>]*>/g, '')),
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -119,7 +128,7 @@ export const parseUser = (data) => {
|
|||
|
||||
output.rights = {
|
||||
moderator: data.pleroma.is_moderator,
|
||||
admin: data.pleroma.is_admin
|
||||
admin: data.pleroma.is_admin,
|
||||
}
|
||||
// TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
|
||||
if (output.rights.admin) {
|
||||
|
|
@ -149,13 +158,10 @@ export const parseUser = (data) => {
|
|||
'moderation_log_read',
|
||||
'announcements_manage_announcements',
|
||||
'emoji_manage_emoji',
|
||||
'statistics_read'
|
||||
'statistics_read',
|
||||
]
|
||||
} else if (data.pleroma.is_moderator) {
|
||||
output.privileges = [
|
||||
'messages_delete',
|
||||
'reports_manage_reports'
|
||||
]
|
||||
output.privileges = ['messages_delete', 'reports_manage_reports']
|
||||
} else {
|
||||
output.privileges = []
|
||||
}
|
||||
|
|
@ -203,7 +209,7 @@ export const parseUser = (data) => {
|
|||
if (data.rights) {
|
||||
output.rights = {
|
||||
moderator: data.rights.delete_others_notice,
|
||||
admin: data.rights.admin
|
||||
admin: data.rights.admin,
|
||||
}
|
||||
}
|
||||
output.no_rich_text = data.no_rich_text
|
||||
|
|
@ -221,7 +227,7 @@ export const parseUser = (data) => {
|
|||
muting: data.muted,
|
||||
blocking: data.statusnet_blocking,
|
||||
followed_by: data.follows_you,
|
||||
following: data.following
|
||||
following: data.following,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,9 +243,10 @@ export const parseUser = (data) => {
|
|||
|
||||
// deactivated was changed to is_active in Pleroma 2.3.0
|
||||
// so check if is_active is present
|
||||
output.deactivated = typeof data.pleroma.is_active !== 'undefined'
|
||||
? !data.pleroma.is_active // new backend
|
||||
: data.pleroma.deactivated // old backend
|
||||
output.deactivated =
|
||||
typeof data.pleroma.is_active !== 'undefined'
|
||||
? !data.pleroma.is_active // new backend
|
||||
: data.pleroma.deactivated // old backend
|
||||
|
||||
output.notification_settings = data.pleroma.notification_settings
|
||||
output.unread_chat_count = data.pleroma.unread_chat_count
|
||||
|
|
@ -324,14 +331,19 @@ export const parseStatus = (data) => {
|
|||
const { pleroma } = data
|
||||
|
||||
if (data.pleroma) {
|
||||
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
|
||||
output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
|
||||
output.text = pleroma.content
|
||||
? data.pleroma.content['text/plain']
|
||||
: data.content
|
||||
output.summary = pleroma.spoiler_text
|
||||
? data.pleroma.spoiler_text['text/plain']
|
||||
: data.spoiler_text
|
||||
output.statusnet_conversation_id = data.pleroma.conversation_id
|
||||
output.is_local = pleroma.local
|
||||
output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct
|
||||
output.thread_muted = pleroma.thread_muted
|
||||
output.emoji_reactions = pleroma.emoji_reactions
|
||||
output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
|
||||
output.parent_visible =
|
||||
pleroma.parent_visible === undefined ? true : pleroma.parent_visible
|
||||
output.quote_visible = pleroma.quote_visible || true
|
||||
output.quotes_count = pleroma.quotes_count
|
||||
output.bookmark_folder_id = pleroma.bookmark_folder
|
||||
|
|
@ -341,9 +353,10 @@ export const parseStatus = (data) => {
|
|||
}
|
||||
|
||||
const quoteRaw = pleroma?.quote || data.quote
|
||||
const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
|
||||
const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
|
||||
output.quote = quoteData
|
||||
output.quote_id = data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma.quote_id
|
||||
output.quote_id =
|
||||
data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma.quote_id
|
||||
output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma.quote_url
|
||||
|
||||
output.in_reply_to_status_id = data.in_reply_to_id
|
||||
|
|
@ -358,9 +371,9 @@ export const parseStatus = (data) => {
|
|||
output.external_url = data.url
|
||||
output.poll = data.poll
|
||||
if (output.poll) {
|
||||
output.poll.options = (output.poll.options || []).map(field => ({
|
||||
output.poll.options = (output.poll.options || []).map((field) => ({
|
||||
...field,
|
||||
title_html: escape(field.title)
|
||||
title_html: escape(field.title),
|
||||
}))
|
||||
}
|
||||
output.pinned = data.pinned
|
||||
|
|
@ -419,10 +432,13 @@ export const parseStatus = (data) => {
|
|||
|
||||
output.user = parseUser(masto ? data.account : data.user)
|
||||
|
||||
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
|
||||
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(
|
||||
parseUser,
|
||||
)
|
||||
|
||||
output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
|
||||
.map(parseAttachment)
|
||||
output.attachments = (
|
||||
(masto ? data.media_attachments : data.attachments) || []
|
||||
).map(parseAttachment)
|
||||
|
||||
const retweetedStatus = masto ? data.reblog : data.retweeted_status
|
||||
if (retweetedStatus) {
|
||||
|
|
@ -442,7 +458,7 @@ export const parseStatus = (data) => {
|
|||
export const parseNotification = (data) => {
|
||||
const mastoDict = {
|
||||
favourite: 'like',
|
||||
reblog: 'repeat'
|
||||
reblog: 'repeat',
|
||||
}
|
||||
const masto = !Object.hasOwn(data, 'ntype')
|
||||
const output = {}
|
||||
|
|
@ -452,10 +468,11 @@ export const parseNotification = (data) => {
|
|||
output.seen = data.pleroma.is_seen
|
||||
// TODO: null check should be a temporary fix, I guess.
|
||||
// Investigate why backend does this.
|
||||
output.status = isStatusNotification(output.type) && data.status !== null ? parseStatus(data.status) : null
|
||||
output.target = output.type !== 'move'
|
||||
? null
|
||||
: parseUser(data.target)
|
||||
output.status =
|
||||
isStatusNotification(output.type) && data.status !== null
|
||||
? parseStatus(data.status)
|
||||
: null
|
||||
output.target = output.type !== 'move' ? null : parseUser(data.target)
|
||||
output.from_profile = parseUser(data.account)
|
||||
output.emoji = data.emoji
|
||||
output.emoji_url = data.emoji_url
|
||||
|
|
@ -470,11 +487,15 @@ export const parseNotification = (data) => {
|
|||
const parsedNotice = parseStatus(data.notice)
|
||||
output.type = data.ntype
|
||||
output.seen = Boolean(data.is_seen)
|
||||
output.status = output.type === 'like'
|
||||
? parseStatus(data.notice.favorited_status)
|
||||
: parsedNotice
|
||||
output.status =
|
||||
output.type === 'like'
|
||||
? parseStatus(data.notice.favorited_status)
|
||||
: parsedNotice
|
||||
output.action = parsedNotice
|
||||
output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
|
||||
output.from_profile =
|
||||
output.type === 'pleroma:chat_mention'
|
||||
? parseUser(data.account)
|
||||
: parseUser(data.from_profile)
|
||||
}
|
||||
|
||||
output.created_at = new Date(data.created_at)
|
||||
|
|
@ -485,7 +506,10 @@ export const parseNotification = (data) => {
|
|||
|
||||
const isNsfw = (status) => {
|
||||
const nsfwRegex = /#nsfw/i
|
||||
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
|
||||
return (
|
||||
(status.tags || []).includes('nsfw') ||
|
||||
!!(status.text || '').match(nsfwRegex)
|
||||
)
|
||||
}
|
||||
|
||||
export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
|
||||
|
|
@ -497,7 +521,7 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
|
|||
|
||||
return {
|
||||
maxId: flakeId ? maxId : parseInt(maxId, 10),
|
||||
minId: flakeId ? minId : parseInt(minId, 10)
|
||||
minId: flakeId ? minId : parseInt(minId, 10),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -512,8 +536,12 @@ export const parseChat = (chat) => {
|
|||
}
|
||||
|
||||
export const parseChatMessage = (message) => {
|
||||
if (!message) { return }
|
||||
if (message.isNormalized) { return message }
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
if (message.isNormalized) {
|
||||
return message
|
||||
}
|
||||
const output = message
|
||||
output.id = message.id
|
||||
output.created_at = new Date(message.created_at)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { capitalize } from 'lodash'
|
||||
|
||||
function humanizeErrors (errors) {
|
||||
function humanizeErrors(errors) {
|
||||
return Object.entries(errors).reduce((errs, [k, val]) => {
|
||||
const message = val.reduce((acc, message) => {
|
||||
const key = capitalize(k.replace(/_/g, ' '))
|
||||
|
|
@ -10,15 +10,17 @@ function humanizeErrors (errors) {
|
|||
}, [])
|
||||
}
|
||||
|
||||
export function StatusCodeError (statusCode, body, options, response) {
|
||||
export function StatusCodeError(statusCode, body, options, response) {
|
||||
this.name = 'StatusCodeError'
|
||||
this.statusCode = statusCode
|
||||
this.message = statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
|
||||
this.message =
|
||||
statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
|
||||
this.error = body // legacy attribute
|
||||
this.options = options
|
||||
this.response = response
|
||||
|
||||
if (Error.captureStackTrace) { // required for non-V8 environments
|
||||
if (Error.captureStackTrace) {
|
||||
// required for non-V8 environments
|
||||
Error.captureStackTrace(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +28,7 @@ StatusCodeError.prototype = Object.create(Error.prototype)
|
|||
StatusCodeError.prototype.constructor = StatusCodeError
|
||||
|
||||
export class RegistrationError extends Error {
|
||||
constructor (error) {
|
||||
constructor(error) {
|
||||
super()
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ export const newExporter = ({
|
|||
filename = 'data',
|
||||
mime = 'application/json',
|
||||
extension = 'json',
|
||||
getExportedObject
|
||||
getExportedObject,
|
||||
}) => ({
|
||||
exportData () {
|
||||
exportData() {
|
||||
let stringified
|
||||
if (mime === 'application/json') {
|
||||
stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces
|
||||
|
|
@ -24,7 +24,7 @@ export const newExporter = ({
|
|||
document.body.appendChild(e)
|
||||
e.click()
|
||||
document.body.removeChild(e)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const newImporter = ({
|
||||
|
|
@ -32,14 +32,14 @@ export const newImporter = ({
|
|||
parser = (string) => JSON.parse(string),
|
||||
onImport,
|
||||
onImportFailure,
|
||||
validator = () => true
|
||||
validator = () => true,
|
||||
}) => ({
|
||||
importData () {
|
||||
importData() {
|
||||
const filePicker = document.createElement('input')
|
||||
filePicker.setAttribute('type', 'file')
|
||||
filePicker.setAttribute('accept', accept)
|
||||
|
||||
filePicker.addEventListener('change', event => {
|
||||
filePicker.addEventListener('change', (event) => {
|
||||
if (event.target.files[0]) {
|
||||
const filename = event.target.files[0].name
|
||||
|
||||
|
|
@ -64,5 +64,5 @@ export const newImporter = ({
|
|||
document.body.appendChild(filePicker)
|
||||
filePicker.click()
|
||||
document.body.removeChild(filePicker)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
const checkCanvasExtractPermission = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 1
|
||||
canvas.height = 1
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return false;
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return false
|
||||
|
||||
ctx.fillStyle = '#0f161e';
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
ctx.fillStyle = '#0f161e'
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
|
||||
const { data } = ctx.getImageData(0, 0, 1, 1);
|
||||
const { data } = ctx.getImageData(0, 0, 1, 1)
|
||||
|
||||
return data.join(',') === '15,22,30,255';
|
||||
};
|
||||
return data.join(',') === '15,22,30,255'
|
||||
}
|
||||
|
||||
const createFaviconService = () => {
|
||||
const favicons = []
|
||||
|
|
@ -21,10 +21,10 @@ const createFaviconService = () => {
|
|||
const badgeRadius = 32
|
||||
|
||||
const initFaviconService = () => {
|
||||
if (!checkCanvasExtractPermission()) return;
|
||||
if (!checkCanvasExtractPermission()) return
|
||||
|
||||
const nodes = document.querySelectorAll('link[rel="icon"]')
|
||||
nodes.forEach(favicon => {
|
||||
nodes.forEach((favicon) => {
|
||||
if (favicon) {
|
||||
const favcanvas = document.createElement('canvas')
|
||||
favcanvas.width = faviconWidth
|
||||
|
|
@ -47,7 +47,17 @@ const createFaviconService = () => {
|
|||
|
||||
favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
|
||||
if (isImageLoaded(favimg)) {
|
||||
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
|
||||
favcontext.drawImage(
|
||||
favimg,
|
||||
0,
|
||||
0,
|
||||
favimg.width,
|
||||
favimg.height,
|
||||
0,
|
||||
0,
|
||||
faviconWidth,
|
||||
faviconHeight,
|
||||
)
|
||||
}
|
||||
favicon.href = favcanvas.toDataURL('image/png')
|
||||
})
|
||||
|
|
@ -63,11 +73,28 @@ const createFaviconService = () => {
|
|||
const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
|
||||
|
||||
if (isImageLoaded(favimg)) {
|
||||
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
|
||||
favcontext.drawImage(
|
||||
favimg,
|
||||
0,
|
||||
0,
|
||||
favimg.width,
|
||||
favimg.height,
|
||||
0,
|
||||
0,
|
||||
faviconWidth,
|
||||
faviconHeight,
|
||||
)
|
||||
}
|
||||
favcontext.fillStyle = badgeColor
|
||||
favcontext.beginPath()
|
||||
favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
|
||||
favcontext.arc(
|
||||
faviconWidth - badgeRadius,
|
||||
badgeRadius,
|
||||
badgeRadius,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
false,
|
||||
)
|
||||
favcontext.fill()
|
||||
favicon.href = favcanvas.toDataURL('image/png')
|
||||
})
|
||||
|
|
@ -79,7 +106,7 @@ const createFaviconService = () => {
|
|||
initFaviconService,
|
||||
clearFaviconBadge,
|
||||
drawFaviconBadge,
|
||||
getOriginalFavicons
|
||||
getOriginalFavicons,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ const fileSizeFormat = (numArg) => {
|
|||
return num + ' ' + units[0]
|
||||
}
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
|
||||
const exponent = Math.min(
|
||||
Math.floor(Math.log(num) / Math.log(1024)),
|
||||
units.length - 1,
|
||||
)
|
||||
num = (num / Math.pow(1024, exponent)).toFixed(2) * 1
|
||||
const unit = units[exponent]
|
||||
return { num, unit }
|
||||
}
|
||||
const fileSizeFormatService = {
|
||||
fileSizeFormat
|
||||
fileSizeFormat,
|
||||
}
|
||||
export default fileSizeFormatService
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// TODO this func might as well take the entire file and use its mimetype
|
||||
// or the entire service could be just mimetype service that only operates
|
||||
// on mimetypes and not files. Currently the naming is confusing.
|
||||
export const fileType = mimetype => {
|
||||
export const fileType = (mimetype) => {
|
||||
if (mimetype.match(/flash/)) {
|
||||
return 'flash'
|
||||
}
|
||||
|
|
@ -25,26 +25,30 @@ export const fileType = mimetype => {
|
|||
return 'unknown'
|
||||
}
|
||||
|
||||
export const fileTypeExt = url => {
|
||||
export const fileTypeExt = (url) => {
|
||||
if (url.match(/\.(a?png|jpe?g|gif|webp|avif)$/)) {
|
||||
return 'image'
|
||||
}
|
||||
if (url.match(/\.(ogv|mp4|webm|mov)$/)) {
|
||||
return 'video'
|
||||
}
|
||||
if (url.match(/\.(it|s3m|mod|umx|mp3|aac|m4a|flac|alac|ogg|oga|opus|wav|ape|midi?)$/)) {
|
||||
if (
|
||||
url.match(
|
||||
/\.(it|s3m|mod|umx|mp3|aac|m4a|flac|alac|ogg|oga|opus|wav|ape|midi?)$/,
|
||||
)
|
||||
) {
|
||||
return 'audio'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export const fileMatchesSomeType = (types, file) =>
|
||||
types.some(type => fileType(file.mimetype) === type)
|
||||
types.some((type) => fileType(file.mimetype) === type)
|
||||
|
||||
const fileTypeService = {
|
||||
fileType,
|
||||
fileTypeExt,
|
||||
fileMatchesSomeType
|
||||
fileMatchesSomeType,
|
||||
}
|
||||
|
||||
export default fileTypeService
|
||||
|
|
|
|||
|
|
@ -1,52 +1,64 @@
|
|||
const fetchRelationship = (attempt, userId, store) => new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
store.state.api.backendInteractor.fetchUserRelationship({ id: userId })
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
return relationship
|
||||
})
|
||||
.then((relationship) => resolve([relationship.following, relationship.requested, relationship.locked, attempt]))
|
||||
.catch((e) => reject(e))
|
||||
}, 500)
|
||||
}).then(([following, sent, locked, attempt]) => {
|
||||
if (!following && !(locked && sent) && attempt <= 3) {
|
||||
// If we BE reports that we still not following that user - retry,
|
||||
// increment attempts by one
|
||||
fetchRelationship(++attempt, userId, store)
|
||||
}
|
||||
})
|
||||
const fetchRelationship = (attempt, userId, store) =>
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
store.state.api.backendInteractor
|
||||
.fetchUserRelationship({ id: userId })
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
return relationship
|
||||
})
|
||||
.then((relationship) =>
|
||||
resolve([
|
||||
relationship.following,
|
||||
relationship.requested,
|
||||
relationship.locked,
|
||||
attempt,
|
||||
]),
|
||||
)
|
||||
.catch((e) => reject(e))
|
||||
}, 500)
|
||||
}).then(([following, sent, locked, attempt]) => {
|
||||
if (!following && !(locked && sent) && attempt <= 3) {
|
||||
// If we BE reports that we still not following that user - retry,
|
||||
// increment attempts by one
|
||||
fetchRelationship(++attempt, userId, store)
|
||||
}
|
||||
})
|
||||
|
||||
export const requestFollow = (userId, store) => new Promise((resolve) => {
|
||||
store.state.api.backendInteractor.followUser({ id: userId })
|
||||
.then((updated) => {
|
||||
store.commit('updateUserRelationship', [updated])
|
||||
export const requestFollow = (userId, store) =>
|
||||
new Promise((resolve) => {
|
||||
store.state.api.backendInteractor
|
||||
.followUser({ id: userId })
|
||||
.then((updated) => {
|
||||
store.commit('updateUserRelationship', [updated])
|
||||
|
||||
if (updated.following || (updated.locked && updated.requested)) {
|
||||
// If we get result immediately or the account is locked, just stop.
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
if (updated.following || (updated.locked && updated.requested)) {
|
||||
// If we get result immediately or the account is locked, just stop.
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// But usually we don't get result immediately, so we ask server
|
||||
// for updated user profile to confirm if we are following them
|
||||
// Sometimes it takes several tries. Sometimes we end up not following
|
||||
// user anyway, probably because they locked themselves and we
|
||||
// don't know that yet.
|
||||
// Recursive Promise, it will call itself up to 3 times.
|
||||
// But usually we don't get result immediately, so we ask server
|
||||
// for updated user profile to confirm if we are following them
|
||||
// Sometimes it takes several tries. Sometimes we end up not following
|
||||
// user anyway, probably because they locked themselves and we
|
||||
// don't know that yet.
|
||||
// Recursive Promise, it will call itself up to 3 times.
|
||||
|
||||
return fetchRelationship(1, updated, store)
|
||||
.then(() => {
|
||||
return fetchRelationship(1, updated, store).then(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export const requestUnfollow = (userId, store) => new Promise((resolve) => {
|
||||
store.state.api.backendInteractor.unfollowUser({ id: userId })
|
||||
.then((updated) => {
|
||||
store.commit('updateUserRelationship', [updated])
|
||||
resolve({
|
||||
updated
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export const requestUnfollow = (userId, store) =>
|
||||
new Promise((resolve) => {
|
||||
store.state.api.backendInteractor
|
||||
.unfollowUser({ id: userId })
|
||||
.then((updated) => {
|
||||
store.commit('updateUserRelationship', [updated])
|
||||
resolve({
|
||||
updated,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ import apiService from '../api/api.service.js'
|
|||
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
||||
|
||||
const fetchAndUpdate = ({ store, credentials }) => {
|
||||
return apiService.fetchFollowRequests({ credentials })
|
||||
.then((requests) => {
|
||||
store.commit('setFollowRequests', requests)
|
||||
store.commit('addNewUsers', requests)
|
||||
}, () => {})
|
||||
return apiService
|
||||
.fetchFollowRequests({ credentials })
|
||||
.then(
|
||||
(requests) => {
|
||||
store.commit('setFollowRequests', requests)
|
||||
store.commit('addNewUsers', requests)
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
|
|
@ -17,7 +21,7 @@ const startFetching = ({ credentials, store }) => {
|
|||
}
|
||||
|
||||
const followRequestFetcher = {
|
||||
startFetching
|
||||
startFetching,
|
||||
}
|
||||
|
||||
export default followRequestFetcher
|
||||
|
|
|
|||
|
|
@ -5,22 +5,25 @@ const DIRECTION_DOWN = [0, 1]
|
|||
|
||||
const BUTTON_LEFT = 0
|
||||
|
||||
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
|
||||
const deltaCoord = (oldCoord, newCoord) => [
|
||||
newCoord[0] - oldCoord[0],
|
||||
newCoord[1] - oldCoord[1],
|
||||
]
|
||||
|
||||
const touchCoord = touch => [touch.screenX, touch.screenY]
|
||||
const touchCoord = (touch) => [touch.screenX, touch.screenY]
|
||||
|
||||
const touchEventCoord = e => touchCoord(e.touches[0])
|
||||
const touchEventCoord = (e) => touchCoord(e.touches[0])
|
||||
|
||||
const pointerEventCoord = e => [e.clientX, e.clientY]
|
||||
const pointerEventCoord = (e) => [e.clientX, e.clientY]
|
||||
|
||||
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
|
||||
const vectorLength = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1])
|
||||
|
||||
const perpendicular = v => [v[1], -v[0]]
|
||||
const perpendicular = (v) => [v[1], -v[0]]
|
||||
|
||||
const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1]
|
||||
|
||||
const project = (v1, v2) => {
|
||||
const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2))
|
||||
const scalar = dotProduct(v1, v2) / dotProduct(v2, v2)
|
||||
return [scalar * v2[0], scalar * v2[1]]
|
||||
}
|
||||
|
||||
|
|
@ -30,14 +33,19 @@ const project = (v1, v2) => {
|
|||
// divergentTolerance: a scalar for much of divergent direction we tolerate when
|
||||
// above threshold. for example, with 1.0 we only call the callback if
|
||||
// divergent component of delta is < 1.0 * direction component of delta.
|
||||
const swipeGesture = (direction, onSwipe, threshold = 30, perpendicularTolerance = 1.0) => {
|
||||
const swipeGesture = (
|
||||
direction,
|
||||
onSwipe,
|
||||
threshold = 30,
|
||||
perpendicularTolerance = 1.0,
|
||||
) => {
|
||||
return {
|
||||
direction,
|
||||
onSwipe,
|
||||
threshold,
|
||||
perpendicularTolerance,
|
||||
_startPos: [0, 0],
|
||||
_swiping: false
|
||||
_swiping: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +68,8 @@ const updateSwipe = (event, gesture) => {
|
|||
if (
|
||||
vectorLength(towardsDir) * gesture.perpendicularTolerance <
|
||||
vectorLength(towardsPerpendicular)
|
||||
) return
|
||||
)
|
||||
return
|
||||
|
||||
gesture.onSwipe()
|
||||
gesture._swiping = false
|
||||
|
|
@ -73,7 +82,7 @@ class SwipeAndClickGesture {
|
|||
// sign: if the swipe does not meet the threshold, 0
|
||||
// if the swipe meets the threshold in the positive direction, 1
|
||||
// if the swipe meets the threshold in the negative direction, -1
|
||||
constructor ({
|
||||
constructor({
|
||||
direction,
|
||||
// swipeStartCallback
|
||||
swipePreviewCallback,
|
||||
|
|
@ -82,7 +91,7 @@ class SwipeAndClickGesture {
|
|||
swipelessClickCallback,
|
||||
threshold = 30,
|
||||
perpendicularTolerance = 1.0,
|
||||
disableClickThreshold = 1
|
||||
disableClickThreshold = 1,
|
||||
}) {
|
||||
const nop = () => {}
|
||||
this.direction = direction
|
||||
|
|
@ -90,13 +99,17 @@ class SwipeAndClickGesture {
|
|||
this.swipeEndCallback = swipeEndCallback || nop
|
||||
this.swipeCancelCallback = swipeCancelCallback || nop
|
||||
this.swipelessClickCallback = swipelessClickCallback || nop
|
||||
this.threshold = typeof threshold === 'function' ? threshold : () => threshold
|
||||
this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
|
||||
this.threshold =
|
||||
typeof threshold === 'function' ? threshold : () => threshold
|
||||
this.disableClickThreshold =
|
||||
typeof disableClickThreshold === 'function'
|
||||
? disableClickThreshold
|
||||
: () => disableClickThreshold
|
||||
this.perpendicularTolerance = perpendicularTolerance
|
||||
this._reset()
|
||||
}
|
||||
|
||||
_reset () {
|
||||
_reset() {
|
||||
this._startPos = [0, 0]
|
||||
this._pointerId = -1
|
||||
this._swiping = false
|
||||
|
|
@ -104,7 +117,7 @@ class SwipeAndClickGesture {
|
|||
this._preventNextClick = false
|
||||
}
|
||||
|
||||
start (event) {
|
||||
start(event) {
|
||||
// Only handle left click
|
||||
if (event.button !== BUTTON_LEFT) {
|
||||
return
|
||||
|
|
@ -116,7 +129,7 @@ class SwipeAndClickGesture {
|
|||
this._swiped = false
|
||||
}
|
||||
|
||||
move (event) {
|
||||
move(event) {
|
||||
if (this._swiping && this._pointerId === event.pointerId) {
|
||||
this._swiped = true
|
||||
|
||||
|
|
@ -127,7 +140,7 @@ class SwipeAndClickGesture {
|
|||
}
|
||||
}
|
||||
|
||||
cancel (event) {
|
||||
cancel(event) {
|
||||
if (!this._swiping || this._pointerId !== event.pointerId) {
|
||||
return
|
||||
}
|
||||
|
|
@ -135,7 +148,7 @@ class SwipeAndClickGesture {
|
|||
this.swipeCancelCallback()
|
||||
}
|
||||
|
||||
end (event) {
|
||||
end(event) {
|
||||
if (!this._swiping) {
|
||||
return
|
||||
}
|
||||
|
|
@ -163,7 +176,7 @@ class SwipeAndClickGesture {
|
|||
const towardsPerpendicular = project(delta, perpendicularDir)
|
||||
if (
|
||||
vectorLength(towardsDir) * this.perpendicularTolerance <
|
||||
vectorLength(towardsPerpendicular)
|
||||
vectorLength(towardsPerpendicular)
|
||||
) {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -179,12 +192,15 @@ class SwipeAndClickGesture {
|
|||
// the end point is far from the starting point
|
||||
// so for other kinds of pointers do not check
|
||||
// whether we have swiped
|
||||
if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
|
||||
if (
|
||||
vectorLength(delta) >= this.disableClickThreshold() &&
|
||||
event.pointerType === 'mouse'
|
||||
) {
|
||||
this._preventNextClick = true
|
||||
}
|
||||
}
|
||||
|
||||
click () {
|
||||
click() {
|
||||
if (!this._preventNextClick) {
|
||||
this.swipelessClickCallback()
|
||||
}
|
||||
|
|
@ -200,7 +216,7 @@ const GestureService = {
|
|||
swipeGesture,
|
||||
beginSwipe,
|
||||
updateSwipe,
|
||||
SwipeAndClickGesture
|
||||
SwipeAndClickGesture,
|
||||
}
|
||||
|
||||
export default GestureService
|
||||
|
|
|
|||
|
|
@ -22,16 +22,58 @@ export const convertHtmlToLines = (html = '') => {
|
|||
// Elements that are implicitly self-closing
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
||||
const emptyElements = new Set([
|
||||
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
||||
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
])
|
||||
// Block-level element (they make a visual line)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
||||
const blockElements = new Set([
|
||||
'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd',
|
||||
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main',
|
||||
'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
|
||||
'address',
|
||||
'article',
|
||||
'aside',
|
||||
'blockquote',
|
||||
'details',
|
||||
'dialog',
|
||||
'dd',
|
||||
'div',
|
||||
'dl',
|
||||
'dt',
|
||||
'fieldset',
|
||||
'figcaption',
|
||||
'figure',
|
||||
'footer',
|
||||
'form',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'header',
|
||||
'hgroup',
|
||||
'hr',
|
||||
'li',
|
||||
'main',
|
||||
'nav',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'section',
|
||||
'table',
|
||||
'ul',
|
||||
])
|
||||
// br is very weird in a way that it's technically not block-level, it's
|
||||
// essentially converted to a \n (or \r\n). There's also wbr but it doesn't
|
||||
|
|
@ -40,7 +82,7 @@ export const convertHtmlToLines = (html = '') => {
|
|||
|
||||
const visualLineElements = new Set([
|
||||
...blockElements.values(),
|
||||
...linebreakElements.values()
|
||||
...linebreakElements.values(),
|
||||
])
|
||||
|
||||
// All block-level elements that aren't empty elements, i.e. not <hr>
|
||||
|
|
@ -53,7 +95,7 @@ export const convertHtmlToLines = (html = '') => {
|
|||
// All elements that we are recognizing
|
||||
const allElements = new Set([
|
||||
...nonEmptyElements.values(),
|
||||
...emptyElements.values()
|
||||
...emptyElements.values(),
|
||||
])
|
||||
|
||||
const buffer = [] // Current output buffer
|
||||
|
|
@ -61,7 +103,8 @@ export const convertHtmlToLines = (html = '') => {
|
|||
let textBuffer = '' // Current line content
|
||||
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
|
||||
|
||||
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
|
||||
const flush = () => {
|
||||
// Processes current line buffer, adds it to output buffer and clears line buffer
|
||||
if (textBuffer.trim().length > 0) {
|
||||
buffer.push({ level: [...level], text: textBuffer })
|
||||
} else {
|
||||
|
|
@ -70,23 +113,27 @@ export const convertHtmlToLines = (html = '') => {
|
|||
textBuffer = ''
|
||||
}
|
||||
|
||||
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
|
||||
const handleBr = (tag) => {
|
||||
// handles single newlines/linebreaks/selfclosing
|
||||
flush()
|
||||
buffer.push(tag)
|
||||
}
|
||||
|
||||
const handleOpen = (tag) => { // handles opening tags
|
||||
const handleOpen = (tag) => {
|
||||
// handles opening tags
|
||||
flush()
|
||||
buffer.push(tag)
|
||||
level.unshift(getTagName(tag))
|
||||
}
|
||||
|
||||
const handleClose = (tag) => { // handles closing tags
|
||||
const handleClose = (tag) => {
|
||||
// handles closing tags
|
||||
if (level[0] === getTagName(tag)) {
|
||||
flush()
|
||||
buffer.push(tag)
|
||||
level.shift()
|
||||
} else { // Broken case
|
||||
} else {
|
||||
// Broken case
|
||||
textBuffer += tag
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,21 @@ export const convertHtmlToTree = (html = '') => {
|
|||
// Elements that are implicitly self-closing
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
||||
const emptyElements = new Set([
|
||||
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
||||
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
])
|
||||
// TODO For future - also parse HTML5 multi-source components?
|
||||
|
||||
|
|
@ -38,7 +51,8 @@ export const convertHtmlToTree = (html = '') => {
|
|||
return levels[levels.length - 1][1]
|
||||
}
|
||||
|
||||
const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
|
||||
const flushText = () => {
|
||||
// Processes current line buffer, adds it to output buffer and clears line buffer
|
||||
if (textBuffer === '') return
|
||||
getCurrentBuffer().push(textBuffer)
|
||||
textBuffer = ''
|
||||
|
|
@ -79,7 +93,10 @@ export const convertHtmlToTree = (html = '') => {
|
|||
const tagName = getTagName(tagFull)
|
||||
if (tagFull[1] === '/') {
|
||||
handleClose(tagFull)
|
||||
} else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
|
||||
} else if (
|
||||
emptyElements.has(tagName) ||
|
||||
tagFull[tagFull.length - 2] === '/'
|
||||
) {
|
||||
// self-closing
|
||||
handleSelfClosing(tagFull)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ export const getAttrs = (tag, filter) => {
|
|||
.replace(new RegExp('^' + getTagName(tag)), '')
|
||||
.replace(/\/?$/, '')
|
||||
.trim()
|
||||
const attrs = Array.from(innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=("[^"]+?"|'[^']+?'))?/gi))
|
||||
const attrs = Array.from(
|
||||
innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=("[^"]+?"|'[^']+?'))?/gi),
|
||||
)
|
||||
.map(([, key, value]) => [key, value])
|
||||
.map(([k, v]) => {
|
||||
if (!v) return [k, true]
|
||||
|
|
@ -59,7 +61,10 @@ export const processTextForEmoji = (text, emojis, processor) => {
|
|||
const next = text.slice(i + 1)
|
||||
let found = false
|
||||
for (const emoji of emojis) {
|
||||
if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
|
||||
if (
|
||||
next.slice(0, emoji.shortcode.length + 1) ===
|
||||
emoji.shortcode + ':'
|
||||
) {
|
||||
found = emoji
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ import apiService from '../api/api.service.js'
|
|||
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
||||
|
||||
const fetchAndUpdate = ({ credentials }) => {
|
||||
return apiService.fetchLists({ credentials })
|
||||
.then(lists => {
|
||||
useListsStore().setLists(lists)
|
||||
}, () => {})
|
||||
return apiService
|
||||
.fetchLists({ credentials })
|
||||
.then(
|
||||
(lists) => {
|
||||
useListsStore().setLists(lists)
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
|
|
@ -17,7 +21,7 @@ const startFetching = ({ credentials, store }) => {
|
|||
}
|
||||
|
||||
const listsFetcher = {
|
||||
startFetching
|
||||
startFetching,
|
||||
}
|
||||
|
||||
export default listsFetcher
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ const specialLanguageCodes = {
|
|||
pdc: 'en',
|
||||
ja_easy: 'ja',
|
||||
zh_Hant: 'zh-HANT',
|
||||
zh: 'zh-Hans'
|
||||
zh: 'zh-Hans',
|
||||
}
|
||||
|
||||
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
|
||||
const internalToBrowserLocale = (code) => specialLanguageCodes[code] || code
|
||||
|
||||
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
|
||||
const internalToBackendLocaleMulti = codes => {
|
||||
const internalToBackendLocale = (code) =>
|
||||
internalToBrowserLocale(code).replace('_', '-')
|
||||
const internalToBackendLocaleMulti = (codes) => {
|
||||
const langs = Array.isArray(codes) ? codes : [codes]
|
||||
return langs.map(internalToBackendLocale).join(',')
|
||||
}
|
||||
|
|
@ -23,21 +24,27 @@ const getLanguageName = (code) => {
|
|||
ja_easy: 'やさしいにほんご',
|
||||
'nan-TW': '臺語(閩南語)',
|
||||
zh: '简体中文',
|
||||
zh_Hant: '繁體中文'
|
||||
zh_Hant: '繁體中文',
|
||||
}
|
||||
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
|
||||
const browserLocale = internalToBrowserLocale(code)
|
||||
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
|
||||
return (
|
||||
languageName.charAt(0).toLocaleUpperCase(browserLocale) +
|
||||
languageName.slice(1)
|
||||
)
|
||||
}
|
||||
|
||||
const languages = _.map(languagesObject.languages, (code) => ({ code, name: getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
|
||||
const languages = _.map(languagesObject.languages, (code) => ({
|
||||
code,
|
||||
name: getLanguageName(code),
|
||||
})).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
const localeService = {
|
||||
internalToBrowserLocale,
|
||||
internalToBackendLocale,
|
||||
internalToBackendLocaleMulti,
|
||||
languages,
|
||||
getLanguageName
|
||||
getLanguageName,
|
||||
}
|
||||
|
||||
export default localeService
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ export const mentionMatchesUrl = (attention, url) => {
|
|||
return true
|
||||
}
|
||||
const [namepart, instancepart] = attention.screen_name.split('@')
|
||||
const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
|
||||
const matchstring = new RegExp(
|
||||
'://' + instancepart + '/.*' + namepart + '$',
|
||||
'g',
|
||||
)
|
||||
|
||||
return !!url.match(matchstring)
|
||||
}
|
||||
|
|
@ -17,7 +20,8 @@ export const extractTagFromUrl = (url) => {
|
|||
const decoded = decodeURI(url)
|
||||
// https://git.pleroma.social/pleroma/elixir-libraries/linkify/-/blob/master/lib/linkify/parser.ex
|
||||
// https://www.pcre.org/original/doc/html/pcrepattern.html
|
||||
const regex = /tag[s]*\/([\p{L}\p{N}_]*[\p{Alphabetic}_·\u{200c}][\p{L}\p{N}_·\p{M}\u{200c}]*)$/ug
|
||||
const regex =
|
||||
/tag[s]*\/([\p{L}\p{N}_]*[\p{Alphabetic}_·\u{200c}][\p{L}\p{N}_·\p{M}\u{200c}]*)$/gu
|
||||
const result = regex.exec(decoded)
|
||||
if (!result) {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
const verifyOTPCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
|
||||
const verifyOTPCode = ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance,
|
||||
mfaToken,
|
||||
code,
|
||||
}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
|
|
@ -8,13 +14,21 @@ const verifyOTPCode = ({ clientId, clientSecret, instance, mfaToken, code }) =>
|
|||
form.append('code', code)
|
||||
form.append('challenge_type', 'totp')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const verifyRecoveryCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
|
||||
const verifyRecoveryCode = ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance,
|
||||
mfaToken,
|
||||
code,
|
||||
}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
|
|
@ -24,15 +38,17 @@ const verifyRecoveryCode = ({ clientId, clientSecret, instance, mfaToken, code }
|
|||
form.append('code', code)
|
||||
form.append('challenge_type', 'recovery')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const mfa = {
|
||||
verifyOTPCode,
|
||||
verifyRecoveryCode
|
||||
verifyRecoveryCode,
|
||||
}
|
||||
|
||||
export default mfa
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ const REDIRECT_URI = `${window.location.origin}/oauth-callback`
|
|||
|
||||
export const getJsonOrError = async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
.catch((error) => {
|
||||
throw new StatusCodeError(response.status, error, {}, response)
|
||||
})
|
||||
return response.json().catch((error) => {
|
||||
throw new StatusCodeError(response.status, error, {}, response)
|
||||
})
|
||||
} else {
|
||||
throw new StatusCodeError(response.status, await response.text(), {}, response)
|
||||
throw new StatusCodeError(
|
||||
response.status,
|
||||
await response.text(),
|
||||
{},
|
||||
response,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,19 +27,24 @@ export const createApp = (instance) => {
|
|||
form.append('redirect_uris', REDIRECT_URI)
|
||||
form.append('scopes', 'read write follow push admin')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
})
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then(getJsonOrError)
|
||||
.then((app) => ({ clientId: app.client_id, clientSecret: app.client_secret }))
|
||||
.then((app) => ({
|
||||
clientId: app.client_id,
|
||||
clientSecret: app.client_secret,
|
||||
}))
|
||||
}
|
||||
|
||||
export const verifyAppToken = ({ instance, appToken }) => {
|
||||
return window.fetch(`${instance}/api/v1/apps/verify_credentials`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${appToken}` }
|
||||
})
|
||||
return window
|
||||
.fetch(`${instance}/api/v1/apps/verify_credentials`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${appToken}` },
|
||||
})
|
||||
.then(getJsonOrError)
|
||||
}
|
||||
|
||||
|
|
@ -44,17 +53,21 @@ const login = ({ instance, clientId }) => {
|
|||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: 'read write follow push admin'
|
||||
scope: 'read write follow push admin',
|
||||
}
|
||||
|
||||
const dataString = reduce(data, (acc, v, k) => {
|
||||
const encoded = `${k}=${encodeURIComponent(v)}`
|
||||
if (!acc) {
|
||||
return encoded
|
||||
} else {
|
||||
return `${acc}&${encoded}`
|
||||
}
|
||||
}, false)
|
||||
const dataString = reduce(
|
||||
data,
|
||||
(acc, v, k) => {
|
||||
const encoded = `${k}=${encodeURIComponent(v)}`
|
||||
if (!acc) {
|
||||
return encoded
|
||||
} else {
|
||||
return `${acc}&${encoded}`
|
||||
}
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
// Do the redirect...
|
||||
const url = `${instance}/oauth/authorize?${dataString}`
|
||||
|
|
@ -62,7 +75,13 @@ const login = ({ instance, clientId }) => {
|
|||
window.location.href = url
|
||||
}
|
||||
|
||||
const getTokenWithCredentials = ({ clientId, clientSecret, instance, username, password }) => {
|
||||
const getTokenWithCredentials = ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance,
|
||||
username,
|
||||
password,
|
||||
}) => {
|
||||
const url = `${instance}/oauth/token`
|
||||
const form = new window.FormData()
|
||||
|
||||
|
|
@ -72,10 +91,12 @@ const getTokenWithCredentials = ({ clientId, clientSecret, instance, username, p
|
|||
form.append('username', username)
|
||||
form.append('password', password)
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const getToken = ({ clientId, clientSecret, instance, code }) => {
|
||||
|
|
@ -88,10 +109,11 @@ const getToken = ({ clientId, clientSecret, instance, code }) => {
|
|||
form.append('code', code)
|
||||
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
})
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
|
|
@ -104,10 +126,12 @@ export const getClientToken = ({ clientId, clientSecret, instance }) => {
|
|||
form.append('grant_type', 'client_credentials')
|
||||
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then(getJsonOrError)
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then(getJsonOrError)
|
||||
}
|
||||
const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
|
|
@ -119,10 +143,12 @@ const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
|
|||
form.append('code', code)
|
||||
form.append('challenge_type', 'totp')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => {
|
||||
|
|
@ -135,10 +161,12 @@ const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => {
|
|||
form.append('code', code)
|
||||
form.append('challenge_type', 'recovery')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const revokeToken = ({ app, instance, token }) => {
|
||||
|
|
@ -149,10 +177,12 @@ const revokeToken = ({ app, instance, token }) => {
|
|||
form.append('client_secret', app.clientSecret)
|
||||
form.append('token', token)
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
return window
|
||||
.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const oauth = {
|
||||
|
|
@ -161,7 +191,7 @@ const oauth = {
|
|||
getTokenWithCredentials,
|
||||
verifyOTPCode,
|
||||
verifyRecoveryCode,
|
||||
revokeToken
|
||||
revokeToken,
|
||||
}
|
||||
|
||||
export default oauth
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ const MASTODON_PASSWORD_RESET_URL = '/auth/password'
|
|||
|
||||
const resetPassword = ({ instance, email }) => {
|
||||
const params = { email }
|
||||
const query = reduce(params, (acc, v, k) => {
|
||||
const encoded = `${k}=${encodeURIComponent(v)}`
|
||||
return `${acc}&${encoded}`
|
||||
}, '')
|
||||
const query = reduce(
|
||||
params,
|
||||
(acc, v, k) => {
|
||||
const encoded = `${k}=${encodeURIComponent(v)}`
|
||||
return `${acc}&${encoded}`
|
||||
},
|
||||
'',
|
||||
)
|
||||
const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}`
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,19 +5,23 @@ import { useAnnouncementsStore } from 'src/stores/announcements'
|
|||
|
||||
import FaviconService from 'src/services/favicon_service/favicon_service.js'
|
||||
|
||||
export const ACTIONABLE_NOTIFICATION_TYPES = new Set(['mention', 'pleroma:report', 'follow_request'])
|
||||
export const ACTIONABLE_NOTIFICATION_TYPES = new Set([
|
||||
'mention',
|
||||
'pleroma:report',
|
||||
'follow_request',
|
||||
])
|
||||
|
||||
let cachedBadgeUrl = null
|
||||
|
||||
export const notificationsFromStore = store => store.state.notifications.data
|
||||
export const notificationsFromStore = (store) => store.state.notifications.data
|
||||
|
||||
export const visibleTypes = store => {
|
||||
export const visibleTypes = (store) => {
|
||||
// When called from within a module we need rootGetters to access wider scope
|
||||
// however when called from a component (i.e. this.$store) we already have wider scope
|
||||
const rootGetters = store.rootGetters || store.getters
|
||||
const { notificationVisibility } = rootGetters.mergedConfig
|
||||
|
||||
return ([
|
||||
return [
|
||||
notificationVisibility.likes && 'like',
|
||||
notificationVisibility.mentions && 'mention',
|
||||
notificationVisibility.statuses && 'status',
|
||||
|
|
@ -27,11 +31,18 @@ export const visibleTypes = store => {
|
|||
notificationVisibility.moves && 'move',
|
||||
notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
|
||||
notificationVisibility.reports && 'pleroma:report',
|
||||
notificationVisibility.polls && 'poll'
|
||||
].filter(_ => _))
|
||||
notificationVisibility.polls && 'poll',
|
||||
].filter((_) => _)
|
||||
}
|
||||
|
||||
const statusNotifications = new Set(['like', 'mention', 'status', 'repeat', 'pleroma:emoji_reaction', 'poll'])
|
||||
const statusNotifications = new Set([
|
||||
'like',
|
||||
'mention',
|
||||
'status',
|
||||
'repeat',
|
||||
'pleroma:emoji_reaction',
|
||||
'poll',
|
||||
])
|
||||
|
||||
export const isStatusNotification = (type) => statusNotifications.has(type)
|
||||
|
||||
|
|
@ -69,22 +80,28 @@ export const maybeShowNotification = (store, notification) => {
|
|||
|
||||
if (notification.seen) return
|
||||
if (!visibleTypes(store).includes(notification.type)) return
|
||||
if (notification.type === 'mention' && isMutedNotification(notification)) return
|
||||
if (notification.type === 'mention' && isMutedNotification(notification))
|
||||
return
|
||||
|
||||
const notificationObject = prepareNotificationObject(notification, useI18nStore().i18n)
|
||||
const notificationObject = prepareNotificationObject(
|
||||
notification,
|
||||
useI18nStore().i18n,
|
||||
)
|
||||
showDesktopNotification(rootState, notificationObject)
|
||||
}
|
||||
|
||||
export const filteredNotificationsFromStore = (store, types) => {
|
||||
// map is just to clone the array since sort mutates it and it causes some issues
|
||||
const sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
|
||||
const sortedNotifications = notificationsFromStore(store)
|
||||
.map((_) => _)
|
||||
.sort(sortById)
|
||||
// TODO implement sorting elsewhere and make it optional
|
||||
return sortedNotifications.filter(
|
||||
(notification) => (types || visibleTypes(store)).includes(notification.type)
|
||||
return sortedNotifications.filter((notification) =>
|
||||
(types || visibleTypes(store)).includes(notification.type),
|
||||
)
|
||||
}
|
||||
|
||||
export const unseenNotificationsFromStore = store => {
|
||||
export const unseenNotificationsFromStore = (store) => {
|
||||
const rootGetters = store.rootGetters || store.getters
|
||||
const ignoreInactionableSeen = rootGetters.mergedConfig.ignoreInactionableSeen
|
||||
|
||||
|
|
@ -109,7 +126,7 @@ export const prepareNotificationObject = (notification, i18n) => {
|
|||
const notifObj = {
|
||||
tag: notification.id,
|
||||
type: notification.type,
|
||||
badge: cachedBadgeUrl
|
||||
badge: cachedBadgeUrl,
|
||||
}
|
||||
const status = notification.status
|
||||
const title = notification.from_profile.name
|
||||
|
|
@ -152,8 +169,13 @@ export const prepareNotificationObject = (notification, i18n) => {
|
|||
}
|
||||
|
||||
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
||||
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
|
||||
status.attachments[0].mimetype.startsWith('image/')) {
|
||||
if (
|
||||
status &&
|
||||
status.attachments &&
|
||||
status.attachments.length > 0 &&
|
||||
!status.nsfw &&
|
||||
status.attachments[0].mimetype.startsWith('image/')
|
||||
) {
|
||||
notifObj.image = status.attachments[0].url
|
||||
}
|
||||
|
||||
|
|
@ -169,8 +191,14 @@ export const countExtraNotifications = (store) => {
|
|||
}
|
||||
|
||||
return [
|
||||
mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0,
|
||||
mergedConfig.showAnnouncementsInExtraNotifications ? useAnnouncementsStore().unreadAnnouncementCount : 0,
|
||||
mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0
|
||||
mergedConfig.showChatsInExtraNotifications
|
||||
? rootGetters.unreadChatCount
|
||||
: 0,
|
||||
mergedConfig.showAnnouncementsInExtraNotifications
|
||||
? useAnnouncementsStore().unreadAnnouncementCount
|
||||
: 0,
|
||||
mergedConfig.showFollowRequestsInExtraNotifications
|
||||
? rootGetters.followRequestCount
|
||||
: 0,
|
||||
].reduce((a, c) => a + c, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,10 @@ const mastoApiNotificationTypes = new Set([
|
|||
'move',
|
||||
'poll',
|
||||
'pleroma:emoji_reaction',
|
||||
'pleroma:report'
|
||||
'pleroma:report',
|
||||
])
|
||||
|
||||
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
||||
|
||||
const args = { credentials }
|
||||
const { getters } = store
|
||||
const rootState = store.rootState || store.state
|
||||
|
|
@ -44,7 +43,10 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
|||
return fetchNotifications({ store, args, older })
|
||||
} else {
|
||||
// fetch new notifications
|
||||
if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) {
|
||||
if (
|
||||
since === undefined &&
|
||||
timelineData.maxId !== Number.POSITIVE_INFINITY
|
||||
) {
|
||||
args.since = timelineData.maxId
|
||||
} else if (since !== null) {
|
||||
args.since = since
|
||||
|
|
@ -57,8 +59,10 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
|||
// we can update the state in this session to mark them as read as well.
|
||||
// The normal maxId-check does not tell if older notifications have changed
|
||||
const notifications = timelineData.data
|
||||
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
|
||||
const unreadNotifsIds = notifications.filter(n => !n.seen).map(n => n.id)
|
||||
const readNotifsIds = notifications.filter((n) => n.seen).map((n) => n.id)
|
||||
const unreadNotifsIds = notifications
|
||||
.filter((n) => !n.seen)
|
||||
.map((n) => n.id)
|
||||
if (readNotifsIds.length > 0 && readNotifsIds.length > 0) {
|
||||
const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification
|
||||
if (minId !== Infinity) {
|
||||
|
|
@ -73,16 +77,19 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
|||
}
|
||||
|
||||
const fetchNotifications = ({ store, args, older }) => {
|
||||
return apiService.fetchTimeline(args)
|
||||
return apiService
|
||||
.fetchTimeline(args)
|
||||
.then((response) => {
|
||||
if (response.errors) {
|
||||
if (response.status === 400 && response.statusText.includes('Invalid value for enum')) {
|
||||
response
|
||||
.statusText
|
||||
if (
|
||||
response.status === 400 &&
|
||||
response.statusText.includes('Invalid value for enum')
|
||||
) {
|
||||
response.statusText
|
||||
.matchAll(/(\w+) - Invalid value for enum./g)
|
||||
.toArray()
|
||||
.map(x => x[1])
|
||||
.forEach(x => mastoApiNotificationTypes.delete(x))
|
||||
.map((x) => x[1])
|
||||
.forEach((x) => mastoApiNotificationTypes.delete(x))
|
||||
return fetchNotifications({ store, args, older })
|
||||
} else {
|
||||
throw new Error(`${response.status} ${response.statusText}`)
|
||||
|
|
@ -97,7 +104,7 @@ const fetchNotifications = ({ store, args, older }) => {
|
|||
level: 'error',
|
||||
messageKey: 'notifications.error',
|
||||
messageArgs: [error.message],
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
})
|
||||
console.error(error)
|
||||
})
|
||||
|
|
@ -115,7 +122,7 @@ const startFetching = ({ credentials, store }) => {
|
|||
|
||||
const notificationsFetcher = {
|
||||
fetchAndUpdate,
|
||||
startFetching
|
||||
startFetching,
|
||||
}
|
||||
|
||||
export default notificationsFetcher
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadding = true) => {
|
||||
export const findOffset = (
|
||||
child,
|
||||
parent,
|
||||
{ top = 0, left = 0 } = {},
|
||||
ignorePadding = true,
|
||||
) => {
|
||||
const result = {
|
||||
top: top + child.offsetTop,
|
||||
left: left + child.offsetLeft
|
||||
left: left + child.offsetLeft,
|
||||
}
|
||||
if (!ignorePadding && child !== window) {
|
||||
const { topPadding, leftPadding } = findPadding(child)
|
||||
|
|
@ -9,7 +14,13 @@ export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadd
|
|||
result.left += ignorePadding ? 0 : leftPadding
|
||||
}
|
||||
|
||||
if (child.offsetParent && window.getComputedStyle(child.offsetParent).position !== 'sticky' && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
|
||||
if (
|
||||
child.offsetParent &&
|
||||
window.getComputedStyle(child.offsetParent).position !== 'sticky' &&
|
||||
(parent === window ||
|
||||
parent.contains(child.offsetParent) ||
|
||||
parent === child.offsetParent)
|
||||
) {
|
||||
return findOffset(child.offsetParent, parent, result, false)
|
||||
} else {
|
||||
if (parent !== window) {
|
||||
|
|
@ -23,9 +34,13 @@ export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadd
|
|||
|
||||
const findPadding = (el) => {
|
||||
const topPaddingStr = window.getComputedStyle(el)['padding-top']
|
||||
const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
|
||||
const topPadding = Number(
|
||||
topPaddingStr.substring(0, topPaddingStr.length - 2),
|
||||
)
|
||||
const leftPaddingStr = window.getComputedStyle(el)['padding-left']
|
||||
const leftPadding = Number(leftPaddingStr.substring(0, leftPaddingStr.length - 2))
|
||||
const leftPadding = Number(
|
||||
leftPaddingStr.substring(0, leftPaddingStr.length - 2),
|
||||
)
|
||||
|
||||
return { topPadding, leftPadding }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const pollFallbackValues = {
|
|||
pollType: 'single',
|
||||
options: ['', ''],
|
||||
expiryAmount: 10,
|
||||
expiryUnit: 'minutes'
|
||||
expiryUnit: 'minutes',
|
||||
}
|
||||
|
||||
const pollFallback = (object, attr) => {
|
||||
|
|
@ -15,10 +15,12 @@ const pollFallback = (object, attr) => {
|
|||
const pollFormToMasto = (poll) => {
|
||||
const expiresIn = DateUtils.unitToSeconds(
|
||||
pollFallback(poll, 'expiryUnit'),
|
||||
pollFallback(poll, 'expiryAmount')
|
||||
pollFallback(poll, 'expiryAmount'),
|
||||
)
|
||||
|
||||
const options = uniq(pollFallback(poll, 'options').filter(option => option !== ''))
|
||||
const options = uniq(
|
||||
pollFallback(poll, 'options').filter((option) => option !== ''),
|
||||
)
|
||||
if (options.length < 2) {
|
||||
return { errorKey: 'polls.not_enough_options' }
|
||||
}
|
||||
|
|
@ -26,11 +28,8 @@ const pollFormToMasto = (poll) => {
|
|||
return {
|
||||
options,
|
||||
multiple: pollFallback(poll, 'pollType') === 'multiple',
|
||||
expiresIn
|
||||
expiresIn,
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
pollFallback,
|
||||
pollFormToMasto
|
||||
}
|
||||
export { pollFallback, pollFormToMasto }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export const promiseInterval = (promiseCall, interval) => {
|
|||
// something unexpected happened and promiseCall did not
|
||||
// return a promise, abort the loop.
|
||||
if (!(promise && promise.finally)) {
|
||||
console.warn('promiseInterval: promise call did not return a promise, stopping interval.')
|
||||
console.warn(
|
||||
'promiseInterval: promise call did not return a promise, stopping interval.',
|
||||
)
|
||||
return
|
||||
}
|
||||
promise.finally(() => {
|
||||
|
|
|
|||
|
|
@ -7,23 +7,24 @@ import { defineAsyncComponent, shallowReactive, h } from 'vue'
|
|||
* this should be done from error component but could be done from loading or
|
||||
* actual target component itself if needs to be.
|
||||
*/
|
||||
function getResettableAsyncComponent (asyncComponent, options) {
|
||||
const asyncComponentFactory = () => () => defineAsyncComponent({
|
||||
loader: asyncComponent,
|
||||
...options
|
||||
})
|
||||
function getResettableAsyncComponent(asyncComponent, options) {
|
||||
const asyncComponentFactory = () => () =>
|
||||
defineAsyncComponent({
|
||||
loader: asyncComponent,
|
||||
...options,
|
||||
})
|
||||
|
||||
const observe = shallowReactive({ c: asyncComponentFactory() })
|
||||
|
||||
return {
|
||||
render () {
|
||||
render() {
|
||||
// emit event resetAsyncComponent to reloading
|
||||
return h(observe.c(), {
|
||||
onResetAsyncComponent () {
|
||||
onResetAsyncComponent() {
|
||||
observe.c = asyncComponentFactory()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,44 @@
|
|||
const createRuffleService = () => {
|
||||
let ruffleInstance = null
|
||||
|
||||
const getRuffle = async () => new Promise((resolve, reject) => {
|
||||
if (ruffleInstance) {
|
||||
resolve(ruffleInstance)
|
||||
return
|
||||
}
|
||||
const getRuffle = async () =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (ruffleInstance) {
|
||||
resolve(ruffleInstance)
|
||||
return
|
||||
}
|
||||
|
||||
// Ruffle needs these to be set before it's loaded
|
||||
// https://github.com/ruffle-rs/ruffle/issues/3952
|
||||
window.RufflePlayer = {}
|
||||
window.RufflePlayer.config = {
|
||||
polyfills: false,
|
||||
publicPath: '/static/ruffle'
|
||||
}
|
||||
// Ruffle needs these to be set before it's loaded
|
||||
// https://github.com/ruffle-rs/ruffle/issues/3952
|
||||
window.RufflePlayer = {}
|
||||
window.RufflePlayer.config = {
|
||||
polyfills: false,
|
||||
publicPath: '/static/ruffle',
|
||||
}
|
||||
|
||||
// Currently it's seems like a better way of loading ruffle
|
||||
// because it needs the wasm publically accessible, but it needs path to it
|
||||
// and filename of wasm seems to be pseudo-randomly generated (is it a hash?)
|
||||
const script = document.createElement('script')
|
||||
// see webpack config, using CopyPlugin to copy it from node_modules
|
||||
// provided via ruffle-mirror
|
||||
script.src = '/static/ruffle/ruffle.js'
|
||||
script.type = 'text/javascript'
|
||||
script.onerror = (e) => { reject(e) }
|
||||
script.onabort = (e) => { reject(e) }
|
||||
script.oncancel = (e) => { reject(e) }
|
||||
script.onload = () => {
|
||||
ruffleInstance = window.RufflePlayer
|
||||
resolve(ruffleInstance)
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
// Currently it's seems like a better way of loading ruffle
|
||||
// because it needs the wasm publically accessible, but it needs path to it
|
||||
// and filename of wasm seems to be pseudo-randomly generated (is it a hash?)
|
||||
const script = document.createElement('script')
|
||||
// see webpack config, using CopyPlugin to copy it from node_modules
|
||||
// provided via ruffle-mirror
|
||||
script.src = '/static/ruffle/ruffle.js'
|
||||
script.type = 'text/javascript'
|
||||
script.onerror = (e) => {
|
||||
reject(e)
|
||||
}
|
||||
script.onabort = (e) => {
|
||||
reject(e)
|
||||
}
|
||||
script.oncancel = (e) => {
|
||||
reject(e)
|
||||
}
|
||||
script.onload = () => {
|
||||
ruffleInstance = window.RufflePlayer
|
||||
resolve(ruffleInstance)
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
|
||||
return { getRuffle }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,58 +3,65 @@ export const muteFilterHits = (muteFilters, status) => {
|
|||
const statusSummary = status.summary.toLowerCase()
|
||||
const replyToUser = status.in_reply_to_screen_name?.toLowerCase()
|
||||
const poster = status.user.screen_name?.toLowerCase()
|
||||
const mentions = (status.attentions || []).map(att => att.screen_name.toLowerCase())
|
||||
const mentions = (status.attentions || []).map((att) =>
|
||||
att.screen_name.toLowerCase(),
|
||||
)
|
||||
|
||||
|
||||
return muteFilters.toSorted((a,b) => b.order - a.order).map(filter => {
|
||||
const { hide, expires, name, value, type, enabled} = filter
|
||||
if (!enabled) return false
|
||||
if (value === '') return false
|
||||
if (expires !== null && expires < Date.now()) return false
|
||||
switch (type) {
|
||||
case 'word': {
|
||||
const lowercaseValue = value.toLowerCase()
|
||||
if (statusText.toLowerCase().includes(lowercaseValue) || statusSummary.toLowerCase().includes(lowercaseValue)) {
|
||||
return { hide, name }
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'regexp': {
|
||||
try {
|
||||
const re = new RegExp(value, 'i')
|
||||
if (re.test(statusText) || re.test(statusSummary)) {
|
||||
return { hide, name }
|
||||
}
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case 'user': {
|
||||
if (
|
||||
poster.includes(value) ||
|
||||
replyToUser.includes(value) ||
|
||||
mentions.some(mention => mention.includes(value))
|
||||
) {
|
||||
return { hide, name }
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'user_regexp': {
|
||||
try {
|
||||
const re = new RegExp(value, 'i')
|
||||
return muteFilters
|
||||
.toSorted((a, b) => b.order - a.order)
|
||||
.map((filter) => {
|
||||
const { hide, expires, name, value, type, enabled } = filter
|
||||
if (!enabled) return false
|
||||
if (value === '') return false
|
||||
if (expires !== null && expires < Date.now()) return false
|
||||
switch (type) {
|
||||
case 'word': {
|
||||
const lowercaseValue = value.toLowerCase()
|
||||
if (
|
||||
re.test(poster) ||
|
||||
re.test(replyToUser) ||
|
||||
mentions.some(mention => re.test(mention))
|
||||
statusText.toLowerCase().includes(lowercaseValue) ||
|
||||
statusSummary.toLowerCase().includes(lowercaseValue)
|
||||
) {
|
||||
return { hide, name }
|
||||
}
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
break
|
||||
}
|
||||
case 'regexp': {
|
||||
try {
|
||||
const re = new RegExp(value, 'i')
|
||||
if (re.test(statusText) || re.test(statusSummary)) {
|
||||
return { hide, name }
|
||||
}
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case 'user': {
|
||||
if (
|
||||
poster.includes(value) ||
|
||||
replyToUser.includes(value) ||
|
||||
mentions.some((mention) => mention.includes(value))
|
||||
) {
|
||||
return { hide, name }
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'user_regexp': {
|
||||
try {
|
||||
const re = new RegExp(value, 'i')
|
||||
if (
|
||||
re.test(poster) ||
|
||||
re.test(replyToUser) ||
|
||||
mentions.some((mention) => re.test(mention))
|
||||
) {
|
||||
return { hide, name }
|
||||
}
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).filter(_ => _)
|
||||
})
|
||||
.filter((_) => _)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,38 +13,39 @@ const postStatus = ({
|
|||
quoteId = undefined,
|
||||
contentType = 'text/plain',
|
||||
preview = false,
|
||||
idempotencyKey = ''
|
||||
idempotencyKey = '',
|
||||
}) => {
|
||||
const mediaIds = map(media, 'id')
|
||||
|
||||
return apiService.postStatus({
|
||||
credentials: store.state.users.currentUser.credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
visibility,
|
||||
sensitive,
|
||||
mediaIds,
|
||||
inReplyToStatusId,
|
||||
quoteId,
|
||||
contentType,
|
||||
poll,
|
||||
preview,
|
||||
idempotencyKey
|
||||
})
|
||||
return apiService
|
||||
.postStatus({
|
||||
credentials: store.state.users.currentUser.credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
visibility,
|
||||
sensitive,
|
||||
mediaIds,
|
||||
inReplyToStatusId,
|
||||
quoteId,
|
||||
contentType,
|
||||
poll,
|
||||
preview,
|
||||
idempotencyKey,
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data.error && !preview) {
|
||||
store.dispatch('addNewStatuses', {
|
||||
statuses: [data],
|
||||
timeline: 'friends',
|
||||
showImmediately: true,
|
||||
noIdUpdate: true // To prevent missing notices on next pull.
|
||||
noIdUpdate: true, // To prevent missing notices on next pull.
|
||||
})
|
||||
}
|
||||
return data
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
error: err.message
|
||||
error: err.message,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -57,27 +58,28 @@ const editStatus = ({
|
|||
sensitive,
|
||||
poll,
|
||||
media = [],
|
||||
contentType = 'text/plain'
|
||||
contentType = 'text/plain',
|
||||
}) => {
|
||||
const mediaIds = map(media, 'id')
|
||||
|
||||
return apiService.editStatus({
|
||||
id: statusId,
|
||||
credentials: store.state.users.currentUser.credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaIds,
|
||||
contentType
|
||||
})
|
||||
return apiService
|
||||
.editStatus({
|
||||
id: statusId,
|
||||
credentials: store.state.users.currentUser.credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaIds,
|
||||
contentType,
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data.error) {
|
||||
store.dispatch('addNewStatuses', {
|
||||
statuses: [data],
|
||||
timeline: 'friends',
|
||||
showImmediately: true,
|
||||
noIdUpdate: true // To prevent missing notices on next pull.
|
||||
noIdUpdate: true, // To prevent missing notices on next pull.
|
||||
})
|
||||
}
|
||||
return data
|
||||
|
|
@ -85,7 +87,7 @@ const editStatus = ({
|
|||
.catch((err) => {
|
||||
console.error('Error editing status', err)
|
||||
return {
|
||||
error: err.message
|
||||
error: err.message,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -104,7 +106,7 @@ const statusPosterService = {
|
|||
postStatus,
|
||||
editStatus,
|
||||
uploadMedia,
|
||||
setMediaDescription
|
||||
setMediaDescription,
|
||||
}
|
||||
|
||||
export default statusPosterService
|
||||
|
|
|
|||
|
|
@ -16,40 +16,40 @@ export const createStyleSheet = (id, priority = 1000) => {
|
|||
rules: [],
|
||||
ready: false,
|
||||
priority,
|
||||
clear () {
|
||||
clear() {
|
||||
this.rules = []
|
||||
},
|
||||
addRule (rule) {
|
||||
addRule(rule) {
|
||||
let newRule = rule
|
||||
if (!CSS.supports?.('backdrop-filter', 'blur()')) {
|
||||
newRule = newRule.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
|
||||
}
|
||||
|
||||
// firefox doesn't like invalid selectors
|
||||
if (!CSS.supports?.('selector(::-webkit-scrollbar)') && newRule.startsWith('::-webkit')) {
|
||||
if (
|
||||
!CSS.supports?.('selector(::-webkit-scrollbar)') &&
|
||||
newRule.startsWith('::-webkit')
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.rules.push(
|
||||
newRule
|
||||
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
|
||||
newRule.replace(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
stylesheets[id] = newStyleSheet
|
||||
return newStyleSheet
|
||||
}
|
||||
|
||||
|
||||
export const adoptStyleSheets = throttle(() => {
|
||||
if (supportsAdoptedStyleSheets) {
|
||||
document.adoptedStyleSheets = Object
|
||||
.values(stylesheets)
|
||||
.filter(x => x.ready)
|
||||
document.adoptedStyleSheets = Object.values(stylesheets)
|
||||
.filter((x) => x.ready)
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(sheet => {
|
||||
.map((sheet) => {
|
||||
const css = new CSSStyleSheet()
|
||||
sheet.rules.forEach(r => css.insertRule(r))
|
||||
sheet.rules.forEach((r) => css.insertRule(r))
|
||||
return css
|
||||
})
|
||||
} else {
|
||||
|
|
@ -59,12 +59,11 @@ export const adoptStyleSheets = throttle(() => {
|
|||
holder.sheet.deleteRule(i)
|
||||
}
|
||||
|
||||
Object
|
||||
.values(stylesheets)
|
||||
.filter(x => x.ready)
|
||||
Object.values(stylesheets)
|
||||
.filter((x) => x.ready)
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.forEach(sheet => {
|
||||
sheet.rules.forEach(r => holder.sheet.insertRule(r))
|
||||
.forEach((sheet) => {
|
||||
sheet.rules.forEach((r) => holder.sheet.insertRule(r))
|
||||
})
|
||||
}
|
||||
// Some older browsers do not support document.adoptedStyleSheets.
|
||||
|
|
@ -73,7 +72,6 @@ export const adoptStyleSheets = throttle(() => {
|
|||
// is nothing to do here.
|
||||
}, 500)
|
||||
|
||||
|
||||
const EAGER_STYLE_ID = 'pleroma-eager-styles'
|
||||
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
|
||||
|
||||
|
|
@ -81,15 +79,15 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
|
|||
const {
|
||||
onNewRule = () => {},
|
||||
onLazyFinished = () => {},
|
||||
onEagerFinished = () => {}
|
||||
onEagerFinished = () => {},
|
||||
} = callbacks
|
||||
|
||||
const themes3 = init({
|
||||
inputRuleset,
|
||||
debug
|
||||
debug,
|
||||
})
|
||||
|
||||
getCssRules(themes3.eager, debug).forEach(rule => {
|
||||
getCssRules(themes3.eager, debug).forEach((rule) => {
|
||||
// Hacks to support multiple selectors on same component
|
||||
onNewRule(rule, false)
|
||||
})
|
||||
|
|
@ -103,8 +101,11 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
|
|||
// let t0 = performance.now()
|
||||
const processChunk = () => {
|
||||
const chunk = chunks[counter]
|
||||
Promise.all(chunk.map(x => x())).then(result => {
|
||||
getCssRules(result.filter(x => x), debug).forEach(rule => {
|
||||
Promise.all(chunk.map((x) => x())).then((result) => {
|
||||
getCssRules(
|
||||
result.filter((x) => x),
|
||||
debug,
|
||||
).forEach((rule) => {
|
||||
onNewRule(rule, true)
|
||||
})
|
||||
// const t1 = performance.now()
|
||||
|
|
@ -131,8 +132,8 @@ export const tryLoadCache = async () => {
|
|||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
|
||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
|
||||
|
||||
cache.data[0].forEach(rule => eagerStyles.addRule(rule))
|
||||
cache.data[1].forEach(rule => lazyStyles.addRule(rule))
|
||||
cache.data[0].forEach((rule) => eagerStyles.addRule(rule))
|
||||
cache.data[1].forEach((rule) => lazyStyles.addRule(rule))
|
||||
|
||||
eagerStyles.ready = true
|
||||
lazyStyles.ready = true
|
||||
|
|
@ -140,7 +141,7 @@ export const tryLoadCache = async () => {
|
|||
console.info(`Loaded theme from cache`)
|
||||
return true
|
||||
} else {
|
||||
console.warn('Engine checksum doesn\'t match, cache not usable, clearing')
|
||||
console.warn("Engine checksum doesn't match, cache not usable, clearing")
|
||||
localStorage.removeItem('pleroma-fe-theme-cache')
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -153,7 +154,7 @@ export const applyTheme = (
|
|||
input,
|
||||
onEagerFinish = () => {},
|
||||
onFinish = () => {},
|
||||
debug
|
||||
debug,
|
||||
) => {
|
||||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
|
||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
|
||||
|
|
@ -164,29 +165,34 @@ export const applyTheme = (
|
|||
const { lazyProcessFunc } = generateTheme(
|
||||
input,
|
||||
{
|
||||
onNewRule (rule, isLazy) {
|
||||
onNewRule(rule, isLazy) {
|
||||
if (isLazy) {
|
||||
lazyStyles.addRule(rule)
|
||||
} else {
|
||||
eagerStyles.addRule(rule)
|
||||
}
|
||||
},
|
||||
onEagerFinished () {
|
||||
onEagerFinished() {
|
||||
eagerStyles.ready = true
|
||||
adoptStyleSheets()
|
||||
onEagerFinish()
|
||||
console.info('Eager part of theme finished, waiting for lazy part to finish to store cache')
|
||||
console.info(
|
||||
'Eager part of theme finished, waiting for lazy part to finish to store cache',
|
||||
)
|
||||
},
|
||||
onLazyFinished () {
|
||||
onLazyFinished() {
|
||||
lazyStyles.ready = true
|
||||
adoptStyleSheets()
|
||||
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
|
||||
const cache = {
|
||||
engineChecksum: getEngineChecksum(),
|
||||
data: [eagerStyles.rules, lazyStyles.rules],
|
||||
}
|
||||
onFinish(cache)
|
||||
localforage.setItem('pleromafe-theme-cache', cache)
|
||||
console.info('Theme cache stored')
|
||||
}
|
||||
},
|
||||
},
|
||||
debug
|
||||
debug,
|
||||
)
|
||||
|
||||
setTimeout(lazyProcessFunc, 0)
|
||||
|
|
@ -202,18 +208,19 @@ const extractStyleConfig = ({
|
|||
navbarSize,
|
||||
panelHeaderSize,
|
||||
textSize,
|
||||
forcedRoundness
|
||||
forcedRoundness,
|
||||
}) => {
|
||||
const result = {
|
||||
sidebarColumnWidth,
|
||||
contentColumnWidth,
|
||||
notifsColumnWidth,
|
||||
themeEditorMinWidth: parseInt(themeEditorMinWidth) === 0 ? 'fit-content' : themeEditorMinWidth,
|
||||
themeEditorMinWidth:
|
||||
parseInt(themeEditorMinWidth) === 0 ? 'fit-content' : themeEditorMinWidth,
|
||||
emojiReactionsScale,
|
||||
emojiSize,
|
||||
navbarSize,
|
||||
panelHeaderSize,
|
||||
textSize
|
||||
textSize,
|
||||
}
|
||||
|
||||
switch (forcedRoundness) {
|
||||
|
|
@ -243,10 +250,10 @@ export const applyConfig = (input) => {
|
|||
return
|
||||
}
|
||||
|
||||
const rules = Object
|
||||
.entries(config)
|
||||
const rules = Object.entries(config)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `--${k}: ${v}`).join(';')
|
||||
.map(([k, v]) => `--${k}: ${v}`)
|
||||
.join(';')
|
||||
|
||||
const styleSheet = createStyleSheet('theme-holder', 30)
|
||||
|
||||
|
|
@ -270,28 +277,27 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
|||
let custom
|
||||
|
||||
const resourceTransform = (resources) => {
|
||||
return Object
|
||||
.entries(resources)
|
||||
.map(([k, v]) => {
|
||||
if (typeof v === 'object') {
|
||||
return [k, () => Promise.resolve(v)]
|
||||
} else if (typeof v === 'string') {
|
||||
return [
|
||||
k,
|
||||
() => window
|
||||
return Object.entries(resources).map(([k, v]) => {
|
||||
if (typeof v === 'object') {
|
||||
return [k, () => Promise.resolve(v)]
|
||||
} else if (typeof v === 'string') {
|
||||
return [
|
||||
k,
|
||||
() =>
|
||||
window
|
||||
.fetch(v, { cache })
|
||||
.then(data => data.text())
|
||||
.then(text => parser(text))
|
||||
.catch(e => {
|
||||
.then((data) => data.text())
|
||||
.then((text) => parser(text))
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
return null
|
||||
})
|
||||
]
|
||||
} else {
|
||||
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
|
||||
return [k, null]
|
||||
}
|
||||
})
|
||||
}),
|
||||
]
|
||||
} else {
|
||||
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
|
||||
return [k, null]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -314,7 +320,11 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
|||
|
||||
const total = [...custom, ...builtin]
|
||||
if (total.length === 0) {
|
||||
return Promise.reject(new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`))
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`Resource at ${url} and ${customUrl} completely unavailable. Panicking`,
|
||||
),
|
||||
)
|
||||
}
|
||||
return Promise.resolve(Object.fromEntries(total))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,89 +1,100 @@
|
|||
/* global process */
|
||||
function urlBase64ToUint8Array (base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
|
||||
const rawData = window.atob(base64)
|
||||
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
|
||||
}
|
||||
|
||||
export function isSWSupported () {
|
||||
export function isSWSupported() {
|
||||
return 'serviceWorker' in navigator
|
||||
}
|
||||
|
||||
function isPushSupported () {
|
||||
function isPushSupported() {
|
||||
return 'PushManager' in window
|
||||
}
|
||||
|
||||
function getOrCreateServiceWorker () {
|
||||
function getOrCreateServiceWorker() {
|
||||
if (!isSWSupported()) return
|
||||
const swType = process.env.HAS_MODULE_SERVICE_WORKER ? 'module' : 'classic'
|
||||
return navigator.serviceWorker.register('/sw-pleroma.js', { type: swType })
|
||||
.catch((err) => console.error('Unable to get or create a service worker.', err))
|
||||
return navigator.serviceWorker
|
||||
.register('/sw-pleroma.js', { type: swType })
|
||||
.catch((err) =>
|
||||
console.error('Unable to get or create a service worker.', err),
|
||||
)
|
||||
}
|
||||
|
||||
function subscribePush (registration, isEnabled, vapidPublicKey) {
|
||||
if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config'))
|
||||
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
|
||||
function subscribePush(registration, isEnabled, vapidPublicKey) {
|
||||
if (!isEnabled)
|
||||
return Promise.reject(new Error('Web Push is disabled in config'))
|
||||
if (!vapidPublicKey)
|
||||
return Promise.reject(new Error('VAPID public key is not found'))
|
||||
|
||||
const subscribeOptions = {
|
||||
userVisibleOnly: false,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
|
||||
}
|
||||
return registration.pushManager.subscribe(subscribeOptions)
|
||||
}
|
||||
|
||||
function unsubscribePush (registration) {
|
||||
return registration.pushManager.getSubscription()
|
||||
.then((subscription) => {
|
||||
if (subscription === null) { return }
|
||||
return subscription.unsubscribe()
|
||||
})
|
||||
function unsubscribePush(registration) {
|
||||
return registration.pushManager.getSubscription().then((subscription) => {
|
||||
if (subscription === null) {
|
||||
return
|
||||
}
|
||||
return subscription.unsubscribe()
|
||||
})
|
||||
}
|
||||
|
||||
function deleteSubscriptionFromBackEnd (token) {
|
||||
function deleteSubscriptionFromBackEnd(token) {
|
||||
return fetch('/api/v1/push/subscription/', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).then((response) => {
|
||||
if (!response.ok) throw new Error('Bad status code from server.')
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
function sendSubscriptionToBackEnd (subscription, token, notificationVisibility) {
|
||||
return window.fetch('/api/v1/push/subscription/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription,
|
||||
data: {
|
||||
alerts: {
|
||||
follow: notificationVisibility.follows,
|
||||
favourite: notificationVisibility.likes,
|
||||
mention: notificationVisibility.mentions,
|
||||
reblog: notificationVisibility.repeats,
|
||||
move: notificationVisibility.moves
|
||||
}
|
||||
}
|
||||
function sendSubscriptionToBackEnd(
|
||||
subscription,
|
||||
token,
|
||||
notificationVisibility,
|
||||
) {
|
||||
return window
|
||||
.fetch('/api/v1/push/subscription/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription,
|
||||
data: {
|
||||
alerts: {
|
||||
follow: notificationVisibility.follows,
|
||||
favourite: notificationVisibility.likes,
|
||||
mention: notificationVisibility.mentions,
|
||||
reblog: notificationVisibility.repeats,
|
||||
move: notificationVisibility.moves,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error('Bad status code from server.')
|
||||
return response.json()
|
||||
})
|
||||
.then((responseData) => {
|
||||
if (!responseData.id) throw new Error('Bad response from server.')
|
||||
return responseData
|
||||
})
|
||||
}).then((response) => {
|
||||
if (!response.ok) throw new Error('Bad status code from server.')
|
||||
return response.json()
|
||||
}).then((responseData) => {
|
||||
if (!responseData.id) throw new Error('Bad response from server.')
|
||||
return responseData
|
||||
})
|
||||
}
|
||||
export async function initServiceWorker (store) {
|
||||
export async function initServiceWorker(store) {
|
||||
if (!isSWSupported()) return
|
||||
await getOrCreateServiceWorker()
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
|
|
@ -97,16 +108,18 @@ export async function initServiceWorker (store) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function showDesktopNotification (content) {
|
||||
export async function showDesktopNotification(content) {
|
||||
if (!isSWSupported) return
|
||||
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
|
||||
const { active: sw } =
|
||||
(await window.navigator.serviceWorker.getRegistration()) || {}
|
||||
if (!sw) return console.error('No serviceworker found!')
|
||||
sw.postMessage({ type: 'desktopNotification', content })
|
||||
}
|
||||
|
||||
export async function closeDesktopNotification ({ id }) {
|
||||
export async function closeDesktopNotification({ id }) {
|
||||
if (!isSWSupported) return
|
||||
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
|
||||
const { active: sw } =
|
||||
(await window.navigator.serviceWorker.getRegistration()) || {}
|
||||
if (!sw) return console.error('No serviceworker found!')
|
||||
if (id >= 0) {
|
||||
sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
|
||||
|
|
@ -115,36 +128,53 @@ export async function closeDesktopNotification ({ id }) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function updateFocus () {
|
||||
export async function updateFocus() {
|
||||
if (!isSWSupported) return
|
||||
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
|
||||
const { active: sw } =
|
||||
(await window.navigator.serviceWorker.getRegistration()) || {}
|
||||
if (!sw) return console.error('No serviceworker found!')
|
||||
sw.postMessage({ type: 'updateFocus' })
|
||||
}
|
||||
|
||||
export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) {
|
||||
export function registerPushNotifications(
|
||||
isEnabled,
|
||||
vapidPublicKey,
|
||||
token,
|
||||
notificationVisibility,
|
||||
) {
|
||||
if (isPushSupported()) {
|
||||
getOrCreateServiceWorker()
|
||||
.then((registration) => subscribePush(registration, isEnabled, vapidPublicKey))
|
||||
.then((subscription) => sendSubscriptionToBackEnd(subscription, token, notificationVisibility))
|
||||
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
|
||||
.then((registration) =>
|
||||
subscribePush(registration, isEnabled, vapidPublicKey),
|
||||
)
|
||||
.then((subscription) =>
|
||||
sendSubscriptionToBackEnd(subscription, token, notificationVisibility),
|
||||
)
|
||||
.catch((e) =>
|
||||
console.warn(`Failed to setup Web Push Notifications: ${e.message}`),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterPushNotifications (token) {
|
||||
export function unregisterPushNotifications(token) {
|
||||
if (isPushSupported()) {
|
||||
Promise.all([
|
||||
deleteSubscriptionFromBackEnd(token),
|
||||
getOrCreateServiceWorker()
|
||||
.then((registration) => {
|
||||
return unsubscribePush(registration).then((result) => [registration, result])
|
||||
return unsubscribePush(registration).then((result) => [
|
||||
registration,
|
||||
result,
|
||||
])
|
||||
})
|
||||
.then(([, unsubResult]) => {
|
||||
if (!unsubResult) {
|
||||
console.warn('Push subscription cancellation wasn\'t successful')
|
||||
console.warn("Push subscription cancellation wasn't successful")
|
||||
}
|
||||
})
|
||||
]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
|
||||
}),
|
||||
]).catch((e) =>
|
||||
console.warn(`Failed to disable Web Push Notifications: ${e.message}`),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { convert } from 'chromatism'
|
|||
|
||||
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
|
||||
|
||||
export const getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha })
|
||||
export const getCssColorString = (color, alpha = 1) =>
|
||||
rgba2css({ ...convert(color).rgb, a: alpha })
|
||||
|
||||
export const getCssShadow = (input, usesDropShadow) => {
|
||||
if (input.length === 0) {
|
||||
|
|
@ -10,16 +11,17 @@ export const getCssShadow = (input, usesDropShadow) => {
|
|||
}
|
||||
|
||||
return input
|
||||
.filter(_ => usesDropShadow ? _.inset : _)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
shad.blur,
|
||||
shad.spread
|
||||
].map(_ => _ + 'px ').concat([
|
||||
getCssColorString(shad.color, shad.alpha),
|
||||
shad.inset ? 'inset' : ''
|
||||
]).join(' ')).join(', ')
|
||||
.filter((_) => (usesDropShadow ? _.inset : _))
|
||||
.map((shad) =>
|
||||
[shad.x, shad.y, shad.blur, shad.spread]
|
||||
.map((_) => _ + 'px ')
|
||||
.concat([
|
||||
getCssColorString(shad.color, shad.alpha),
|
||||
shad.inset ? 'inset' : '',
|
||||
])
|
||||
.join(' '),
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
export const getCssShadowFilter = (input) => {
|
||||
|
|
@ -27,124 +29,156 @@ export const getCssShadowFilter = (input) => {
|
|||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
// drop-shadow doesn't support inset or spread
|
||||
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
// drop-shadow's blur is twice as strong compared to box-shadow
|
||||
shad.blur / 2
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColorString(shad.color, shad.alpha)
|
||||
]).join(' '))
|
||||
.map(_ => `drop-shadow(${_})`)
|
||||
.join(' ')
|
||||
return (
|
||||
input
|
||||
// drop-shadow doesn't support inset or spread
|
||||
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
||||
.map((shad) =>
|
||||
[
|
||||
shad.x,
|
||||
shad.y,
|
||||
// drop-shadow's blur is twice as strong compared to box-shadow
|
||||
shad.blur / 2,
|
||||
]
|
||||
.map((_) => _ + 'px')
|
||||
.concat([getCssColorString(shad.color, shad.alpha)])
|
||||
.join(' '),
|
||||
)
|
||||
.map((_) => `drop-shadow(${_})`)
|
||||
.join(' ')
|
||||
)
|
||||
}
|
||||
|
||||
// `debug` changes what backgrounds are used to "stacked" solid colors so you can see
|
||||
// what theme engine "thinks" is actual background color is for purposes of text color
|
||||
// generation and for when --stacked variable is used
|
||||
export const getCssRules = (rules, debug) => rules.map(rule => {
|
||||
let selector = rule.selector
|
||||
if (!selector) {
|
||||
selector = 'html'
|
||||
}
|
||||
const header = selector + ' {'
|
||||
const footer = '}'
|
||||
|
||||
const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => {
|
||||
return ' ' + k + ': ' + v
|
||||
}).join(';\n')
|
||||
|
||||
const directives = Object.entries(rule.directives).map(([k, v]) => {
|
||||
switch (k) {
|
||||
case 'roundness': {
|
||||
return ' ' + [
|
||||
'--roundness: ' + v + 'px'
|
||||
].join(';\n ')
|
||||
export const getCssRules = (rules, debug) =>
|
||||
rules
|
||||
.map((rule) => {
|
||||
let selector = rule.selector
|
||||
if (!selector) {
|
||||
selector = 'html'
|
||||
}
|
||||
case 'shadow': {
|
||||
if (!rule.dynamicVars.shadow) {
|
||||
return ''
|
||||
}
|
||||
return ' ' + [
|
||||
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
|
||||
'--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow),
|
||||
'--shadowInset: ' + getCssShadow(rule.dynamicVars.shadow, true)
|
||||
].join(';\n ')
|
||||
}
|
||||
case 'background': {
|
||||
if (debug) {
|
||||
return `
|
||||
const header = selector + ' {'
|
||||
const footer = '}'
|
||||
|
||||
const virtualDirectives = Object.entries(rule.virtualDirectives || {})
|
||||
.map(([k, v]) => {
|
||||
return ' ' + k + ': ' + v
|
||||
})
|
||||
.join(';\n')
|
||||
|
||||
const directives = Object.entries(rule.directives)
|
||||
.map(([k, v]) => {
|
||||
switch (k) {
|
||||
case 'roundness': {
|
||||
return ' ' + ['--roundness: ' + v + 'px'].join(';\n ')
|
||||
}
|
||||
case 'shadow': {
|
||||
if (!rule.dynamicVars.shadow) {
|
||||
return ''
|
||||
}
|
||||
return (
|
||||
' ' +
|
||||
[
|
||||
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
|
||||
'--shadowFilter: ' +
|
||||
getCssShadowFilter(rule.dynamicVars.shadow),
|
||||
'--shadowInset: ' +
|
||||
getCssShadow(rule.dynamicVars.shadow, true),
|
||||
].join(';\n ')
|
||||
)
|
||||
}
|
||||
case 'background': {
|
||||
if (debug) {
|
||||
return `
|
||||
--background: ${getCssColorString(rule.dynamicVars.stacked)};
|
||||
background-color: ${getCssColorString(rule.dynamicVars.stacked)};
|
||||
`
|
||||
}
|
||||
if (v === 'transparent') {
|
||||
if (rule.component === 'Root') return null
|
||||
return [
|
||||
rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '',
|
||||
' --background: ' + v
|
||||
].filter(x => x).join(';\n')
|
||||
}
|
||||
const color = getCssColorString(rule.dynamicVars.background, rule.directives.opacity)
|
||||
const cssDirectives = ['--background: ' + color]
|
||||
if (rule.directives.backgroundNoCssColor !== 'yes') {
|
||||
cssDirectives.push('background-color: ' + color)
|
||||
}
|
||||
return cssDirectives.filter(x => x).join(';\n')
|
||||
}
|
||||
case 'blur': {
|
||||
const cssDirectives = []
|
||||
if (rule.directives.opacity < 1) {
|
||||
cssDirectives.push(`--backdrop-filter: blur(${v}) `)
|
||||
if (rule.directives.backgroundNoCssColor !== 'yes') {
|
||||
cssDirectives.push(`backdrop-filter: blur(${v}) `)
|
||||
}
|
||||
}
|
||||
return cssDirectives.join(';\n')
|
||||
}
|
||||
case 'font': {
|
||||
return 'font-family: ' + v
|
||||
}
|
||||
case 'textColor': {
|
||||
if (rule.directives.textNoCssColor === 'yes') { return '' }
|
||||
return 'color: ' + v
|
||||
}
|
||||
default:
|
||||
if (k.startsWith('--')) {
|
||||
const [type, value] = v.split('|').map(x => x.trim())
|
||||
switch (type) {
|
||||
case 'color': {
|
||||
const color = rule.dynamicVars[k]
|
||||
if (typeof color === 'string') {
|
||||
return k + ': ' + rgba2css(hex2rgb(color))
|
||||
} else {
|
||||
return k + ': ' + rgba2css(color)
|
||||
}
|
||||
if (v === 'transparent') {
|
||||
if (rule.component === 'Root') return null
|
||||
return [
|
||||
rule.directives.backgroundNoCssColor !== 'yes'
|
||||
? 'background-color: ' + v
|
||||
: '',
|
||||
' --background: ' + v,
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join(';\n')
|
||||
}
|
||||
const color = getCssColorString(
|
||||
rule.dynamicVars.background,
|
||||
rule.directives.opacity,
|
||||
)
|
||||
const cssDirectives = ['--background: ' + color]
|
||||
if (rule.directives.backgroundNoCssColor !== 'yes') {
|
||||
cssDirectives.push('background-color: ' + color)
|
||||
}
|
||||
return cssDirectives.filter((x) => x).join(';\n')
|
||||
}
|
||||
case 'blur': {
|
||||
const cssDirectives = []
|
||||
if (rule.directives.opacity < 1) {
|
||||
cssDirectives.push(`--backdrop-filter: blur(${v}) `)
|
||||
if (rule.directives.backgroundNoCssColor !== 'yes') {
|
||||
cssDirectives.push(`backdrop-filter: blur(${v}) `)
|
||||
}
|
||||
}
|
||||
return cssDirectives.join(';\n')
|
||||
}
|
||||
case 'font': {
|
||||
return 'font-family: ' + v
|
||||
}
|
||||
case 'textColor': {
|
||||
if (rule.directives.textNoCssColor === 'yes') {
|
||||
return ''
|
||||
}
|
||||
return 'color: ' + v
|
||||
}
|
||||
case 'generic':
|
||||
return k + ': ' + value
|
||||
default:
|
||||
if (k.startsWith('--')) {
|
||||
const [type, value] = v.split('|').map((x) => x.trim())
|
||||
switch (type) {
|
||||
case 'color': {
|
||||
const color = rule.dynamicVars[k]
|
||||
if (typeof color === 'string') {
|
||||
return k + ': ' + rgba2css(hex2rgb(color))
|
||||
} else {
|
||||
return k + ': ' + rgba2css(color)
|
||||
}
|
||||
}
|
||||
case 'generic':
|
||||
return k + ': ' + value
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}).filter(x => x).map(x => ' ' + x + ';').join('\n')
|
||||
})
|
||||
.filter((x) => x)
|
||||
.map((x) => ' ' + x + ';')
|
||||
.join('\n')
|
||||
|
||||
return [
|
||||
header,
|
||||
directives,
|
||||
(rule.component === 'Text' && rule.state.indexOf('faint') < 0 && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '',
|
||||
virtualDirectives,
|
||||
footer
|
||||
].filter(x => x).join('\n')
|
||||
}).filter(x => x)
|
||||
return [
|
||||
header,
|
||||
directives,
|
||||
rule.component === 'Text' &&
|
||||
rule.state.indexOf('faint') < 0 &&
|
||||
rule.directives.textNoCssColor !== 'yes'
|
||||
? ' color: var(--text);'
|
||||
: '',
|
||||
virtualDirectives,
|
||||
footer,
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join('\n')
|
||||
})
|
||||
.filter((x) => x)
|
||||
|
||||
export const getScopedVersion = (rules, newScope) => {
|
||||
return rules.map(x => {
|
||||
return rules.map((x) => {
|
||||
if (x.startsWith('html')) {
|
||||
return x.replace('html', newScope)
|
||||
} else if (x.startsWith('#content')) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
import { flattenDeep } from 'lodash'
|
||||
|
||||
export const deserializeShadow = string => {
|
||||
const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha', 'name']
|
||||
export const deserializeShadow = (string) => {
|
||||
const modes = [
|
||||
'_full',
|
||||
'inset',
|
||||
'x',
|
||||
'y',
|
||||
'blur',
|
||||
'spread',
|
||||
'color',
|
||||
'alpha',
|
||||
'name',
|
||||
]
|
||||
const regexPrep = [
|
||||
// inset keyword (optional)
|
||||
'^',
|
||||
|
|
@ -20,7 +30,7 @@ export const deserializeShadow = string => {
|
|||
'(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?',
|
||||
// name
|
||||
'(?:\\s+#(\\w+)\\s*)?',
|
||||
'$'
|
||||
'$',
|
||||
].join('')
|
||||
const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string
|
||||
const result = regex.exec(string)
|
||||
|
|
@ -32,20 +42,26 @@ export const deserializeShadow = string => {
|
|||
}
|
||||
} else {
|
||||
const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
|
||||
const { x, y, blur, spread, alpha, inset, color, name } = Object.fromEntries(modes.map((mode, i) => {
|
||||
if (numeric.has(mode)) {
|
||||
const number = Number(result[i])
|
||||
if (Number.isNaN(number)) {
|
||||
if (mode === 'alpha') return [mode, 1]
|
||||
return [mode, 0]
|
||||
}
|
||||
return [mode, number]
|
||||
} else if (mode === 'inset') {
|
||||
return [mode, !!result[i]]
|
||||
} else {
|
||||
return [mode, result[i]]
|
||||
}
|
||||
}).filter(([, v]) => v !== false).slice(1))
|
||||
const { x, y, blur, spread, alpha, inset, color, name } =
|
||||
Object.fromEntries(
|
||||
modes
|
||||
.map((mode, i) => {
|
||||
if (numeric.has(mode)) {
|
||||
const number = Number(result[i])
|
||||
if (Number.isNaN(number)) {
|
||||
if (mode === 'alpha') return [mode, 1]
|
||||
return [mode, 0]
|
||||
}
|
||||
return [mode, number]
|
||||
} else if (mode === 'inset') {
|
||||
return [mode, !!result[i]]
|
||||
} else {
|
||||
return [mode, result[i]]
|
||||
}
|
||||
})
|
||||
.filter(([, v]) => v !== false)
|
||||
.slice(1),
|
||||
)
|
||||
|
||||
return { x, y, blur, spread, color, alpha, inset, name }
|
||||
}
|
||||
|
|
@ -94,77 +110,87 @@ const parseIss = (input) => {
|
|||
}
|
||||
export const deserialize = (input) => {
|
||||
const ast = parseIss(input)
|
||||
const finalResult = ast.filter(i => i.selector != null).map(item => {
|
||||
const { selector, content } = item
|
||||
let stateCount = 0
|
||||
const selectors = selector.split(/,/g)
|
||||
const result = selectors.map(selector => {
|
||||
const output = { component: '' }
|
||||
let currentDepth = null
|
||||
const finalResult = ast
|
||||
.filter((i) => i.selector != null)
|
||||
.map((item) => {
|
||||
const { selector, content } = item
|
||||
let stateCount = 0
|
||||
const selectors = selector.split(/,/g)
|
||||
const result = selectors.map((selector) => {
|
||||
const output = { component: '' }
|
||||
let currentDepth = null
|
||||
|
||||
selector.split(/ /g).reverse().forEach((fragment, index, arr) => {
|
||||
const fragmentObject = { component: '' }
|
||||
selector
|
||||
.split(/ /g)
|
||||
.reverse()
|
||||
.forEach((fragment, index, arr) => {
|
||||
const fragmentObject = { component: '' }
|
||||
|
||||
let mode = 'component'
|
||||
for (let i = 0; i < fragment.length; i++) {
|
||||
const char = fragment[i]
|
||||
switch (char) {
|
||||
case '.': {
|
||||
mode = 'variant'
|
||||
fragmentObject.variant = ''
|
||||
break
|
||||
}
|
||||
case ':': {
|
||||
mode = 'state'
|
||||
fragmentObject.state = fragmentObject.state || []
|
||||
stateCount++
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (mode === 'state') {
|
||||
const currentState = fragmentObject.state[stateCount - 1]
|
||||
if (currentState == null) {
|
||||
fragmentObject.state.push('')
|
||||
let mode = 'component'
|
||||
for (let i = 0; i < fragment.length; i++) {
|
||||
const char = fragment[i]
|
||||
switch (char) {
|
||||
case '.': {
|
||||
mode = 'variant'
|
||||
fragmentObject.variant = ''
|
||||
break
|
||||
}
|
||||
case ':': {
|
||||
mode = 'state'
|
||||
fragmentObject.state = fragmentObject.state || []
|
||||
stateCount++
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (mode === 'state') {
|
||||
const currentState = fragmentObject.state[stateCount - 1]
|
||||
if (currentState == null) {
|
||||
fragmentObject.state.push('')
|
||||
}
|
||||
fragmentObject.state[stateCount - 1] += char
|
||||
} else {
|
||||
fragmentObject[mode] += char
|
||||
}
|
||||
}
|
||||
fragmentObject.state[stateCount - 1] += char
|
||||
} else {
|
||||
fragmentObject[mode] += char
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentDepth !== null) {
|
||||
currentDepth.parent = { ...fragmentObject }
|
||||
currentDepth = currentDepth.parent
|
||||
} else {
|
||||
Object.keys(fragmentObject).forEach(key => {
|
||||
output[key] = fragmentObject[key]
|
||||
if (currentDepth !== null) {
|
||||
currentDepth.parent = { ...fragmentObject }
|
||||
currentDepth = currentDepth.parent
|
||||
} else {
|
||||
Object.keys(fragmentObject).forEach((key) => {
|
||||
output[key] = fragmentObject[key]
|
||||
})
|
||||
if (index !== arr.length - 1) {
|
||||
output.parent = { component: '' }
|
||||
}
|
||||
currentDepth = output
|
||||
}
|
||||
})
|
||||
if (index !== (arr.length - 1)) {
|
||||
output.parent = { component: '' }
|
||||
}
|
||||
currentDepth = output
|
||||
}
|
||||
|
||||
output.directives = Object.fromEntries(
|
||||
content.map((d) => {
|
||||
const [property, value] = d.split(':')
|
||||
let realValue = (value || '').trim()
|
||||
if (property === 'shadow') {
|
||||
if (realValue === 'none') {
|
||||
realValue = []
|
||||
} else {
|
||||
realValue = value
|
||||
.split(',')
|
||||
.map((v) => deserializeShadow(v.trim()))
|
||||
}
|
||||
}
|
||||
if (!Number.isNaN(Number(value))) {
|
||||
realValue = Number(value)
|
||||
}
|
||||
return [property, realValue]
|
||||
}),
|
||||
)
|
||||
|
||||
return output
|
||||
})
|
||||
|
||||
output.directives = Object.fromEntries(content.map(d => {
|
||||
const [property, value] = d.split(':')
|
||||
let realValue = (value || '').trim()
|
||||
if (property === 'shadow') {
|
||||
if (realValue === 'none') {
|
||||
realValue = []
|
||||
} else {
|
||||
realValue = value.split(',').map(v => deserializeShadow(v.trim()))
|
||||
}
|
||||
} if (!Number.isNaN(Number(value))) {
|
||||
realValue = Number(value)
|
||||
}
|
||||
return [property, realValue]
|
||||
}))
|
||||
|
||||
return output
|
||||
return result
|
||||
})
|
||||
return result
|
||||
})
|
||||
return flattenDeep(finalResult)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,40 +14,54 @@ export const serializeShadow = (s) => {
|
|||
}
|
||||
|
||||
export const serialize = (ruleset) => {
|
||||
return ruleset.map((rule) => {
|
||||
if (Object.keys(rule.directives || {}).length === 0) return false
|
||||
return ruleset
|
||||
.map((rule) => {
|
||||
if (Object.keys(rule.directives || {}).length === 0) return false
|
||||
|
||||
const header = unroll(rule).reverse().map(rule => {
|
||||
const { component } = rule
|
||||
const newVariant = (rule.variant == null || rule.variant === 'normal') ? '' : ('.' + rule.variant)
|
||||
const newState = (rule.state || []).filter(st => st !== 'normal')
|
||||
const header = unroll(rule)
|
||||
.reverse()
|
||||
.map((rule) => {
|
||||
const { component } = rule
|
||||
const newVariant =
|
||||
rule.variant == null || rule.variant === 'normal'
|
||||
? ''
|
||||
: '.' + rule.variant
|
||||
const newState = (rule.state || []).filter((st) => st !== 'normal')
|
||||
|
||||
return `${component}${newVariant}${newState.map(st => ':' + st).join('')}`
|
||||
}).join(' ')
|
||||
return `${component}${newVariant}${newState.map((st) => ':' + st).join('')}`
|
||||
})
|
||||
.join(' ')
|
||||
|
||||
const content = Object.entries(rule.directives).map(([directive, value]) => {
|
||||
if (directive.startsWith('--')) {
|
||||
const [valType, newValue] = value.split('|') // only first one! intentional!
|
||||
switch (valType) {
|
||||
case 'shadow':
|
||||
return ` ${directive}: ${valType.trim()} | ${newValue.map(serializeShadow).map(s => s.trim()).join(', ')}`
|
||||
default:
|
||||
return ` ${directive}: ${valType.trim()} | ${newValue.trim()}`
|
||||
}
|
||||
} else {
|
||||
switch (directive) {
|
||||
case 'shadow':
|
||||
if (value.length > 0) {
|
||||
return ` ${directive}: ${value.map(serializeShadow).join(', ')}`
|
||||
} else {
|
||||
return ` ${directive}: none`
|
||||
const content = Object.entries(rule.directives).map(
|
||||
([directive, value]) => {
|
||||
if (directive.startsWith('--')) {
|
||||
const [valType, newValue] = value.split('|') // only first one! intentional!
|
||||
switch (valType) {
|
||||
case 'shadow':
|
||||
return ` ${directive}: ${valType.trim()} | ${newValue
|
||||
.map(serializeShadow)
|
||||
.map((s) => s.trim())
|
||||
.join(', ')}`
|
||||
default:
|
||||
return ` ${directive}: ${valType.trim()} | ${newValue.trim()}`
|
||||
}
|
||||
default:
|
||||
return ` ${directive}: ${value}`
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
switch (directive) {
|
||||
case 'shadow':
|
||||
if (value.length > 0) {
|
||||
return ` ${directive}: ${value.map(serializeShadow).join(', ')}`
|
||||
} else {
|
||||
return ` ${directive}: none`
|
||||
}
|
||||
default:
|
||||
return ` ${directive}: ${value}`
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return `${header} {\n${content.join(';\n')}\n}`
|
||||
}).filter(x => x).join('\n\n')
|
||||
return `${header} {\n${content.join(';\n')}\n}`
|
||||
})
|
||||
.filter((x) => x)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,18 +15,18 @@ export const unroll = (item) => {
|
|||
// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
|
||||
// Can only accept primitives. Duplicates are not supported and can cause unexpected behavior
|
||||
export const getAllPossibleCombinations = (array) => {
|
||||
const combos = [array.map(x => [x])]
|
||||
const combos = [array.map((x) => [x])]
|
||||
for (let comboSize = 2; comboSize <= array.length; comboSize++) {
|
||||
const previous = combos[combos.length - 1]
|
||||
const newCombos = previous.map(self => {
|
||||
const newCombos = previous.map((self) => {
|
||||
const selfSet = new Set()
|
||||
self.forEach(x => selfSet.add(x))
|
||||
const nonSelf = array.filter(x => !selfSet.has(x))
|
||||
return nonSelf.map(x => [...self, x])
|
||||
self.forEach((x) => selfSet.add(x))
|
||||
const nonSelf = array.filter((x) => !selfSet.has(x))
|
||||
return nonSelf.map((x) => [...self, x])
|
||||
})
|
||||
const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], [])
|
||||
const uniqueComboStrings = new Set()
|
||||
const uniqueCombos = flatCombos.map(sortBy).filter(x => {
|
||||
const uniqueCombos = flatCombos.map(sortBy).filter((x) => {
|
||||
if (uniqueComboStrings.has(x.join())) {
|
||||
return false
|
||||
} else {
|
||||
|
|
@ -56,75 +56,98 @@ export const getAllPossibleCombinations = (array) => {
|
|||
*
|
||||
* @returns {String} CSS selector (or path)
|
||||
*/
|
||||
export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
|
||||
const isParent = !!children
|
||||
if (!rule && !isParent) return null
|
||||
const component = components[rule.component]
|
||||
const { states = {}, variants = {}, outOfTreeSelector } = component
|
||||
export const genericRuleToSelector =
|
||||
(components) => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
|
||||
const isParent = !!children
|
||||
if (!rule && !isParent) return null
|
||||
const component = components[rule.component]
|
||||
const { states = {}, variants = {}, outOfTreeSelector } = component
|
||||
|
||||
const expand = (array = [], subArray = []) => {
|
||||
if (array.length === 0) return subArray.map(x => [x])
|
||||
if (subArray.length === 0) return array.map(x => [x])
|
||||
return array.map(a => {
|
||||
return subArray.map(b => [a, b])
|
||||
}).flat()
|
||||
}
|
||||
|
||||
let componentSelectors = Array.isArray(component.selector) ? component.selector : [component.selector]
|
||||
if (ignoreOutOfTreeSelector || liteMode) componentSelectors = [componentSelectors[0]]
|
||||
componentSelectors = componentSelectors.map(selector => {
|
||||
if (selector === ':root') {
|
||||
return ''
|
||||
} else if (isParent) {
|
||||
return selector
|
||||
} else {
|
||||
if (outOfTreeSelector && !ignoreOutOfTreeSelector) return outOfTreeSelector
|
||||
return selector
|
||||
const expand = (array = [], subArray = []) => {
|
||||
if (array.length === 0) return subArray.map((x) => [x])
|
||||
if (subArray.length === 0) return array.map((x) => [x])
|
||||
return array
|
||||
.map((a) => {
|
||||
return subArray.map((b) => [a, b])
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
})
|
||||
|
||||
const applicableVariantName = (rule.variant || 'normal')
|
||||
let variantSelectors = null
|
||||
if (applicableVariantName !== 'normal') {
|
||||
variantSelectors = variants[applicableVariantName]
|
||||
} else {
|
||||
variantSelectors = variants?.normal ?? ''
|
||||
let componentSelectors = Array.isArray(component.selector)
|
||||
? component.selector
|
||||
: [component.selector]
|
||||
if (ignoreOutOfTreeSelector || liteMode)
|
||||
componentSelectors = [componentSelectors[0]]
|
||||
componentSelectors = componentSelectors.map((selector) => {
|
||||
if (selector === ':root') {
|
||||
return ''
|
||||
} else if (isParent) {
|
||||
return selector
|
||||
} else {
|
||||
if (outOfTreeSelector && !ignoreOutOfTreeSelector)
|
||||
return outOfTreeSelector
|
||||
return selector
|
||||
}
|
||||
})
|
||||
|
||||
const applicableVariantName = rule.variant || 'normal'
|
||||
let variantSelectors = null
|
||||
if (applicableVariantName !== 'normal') {
|
||||
variantSelectors = variants[applicableVariantName]
|
||||
} else {
|
||||
variantSelectors = variants?.normal ?? ''
|
||||
}
|
||||
variantSelectors = Array.isArray(variantSelectors)
|
||||
? variantSelectors
|
||||
: [variantSelectors]
|
||||
if (ignoreOutOfTreeSelector || liteMode)
|
||||
variantSelectors = [variantSelectors[0]]
|
||||
|
||||
const applicableStates = (rule.state || []).filter((x) => x !== 'normal')
|
||||
// const applicableStates = (rule.state || [])
|
||||
const statesSelectors = applicableStates.map((state) => {
|
||||
const selector = states[state] || ''
|
||||
let arraySelector = Array.isArray(selector) ? selector : [selector]
|
||||
if (ignoreOutOfTreeSelector || liteMode)
|
||||
arraySelector = [arraySelector[0]]
|
||||
arraySelector
|
||||
.sort((a) => {
|
||||
if (a.startsWith(':')) return 1
|
||||
if (/^[a-z]/.exec(a)) return -1
|
||||
else return 0
|
||||
})
|
||||
.join('')
|
||||
return arraySelector
|
||||
})
|
||||
|
||||
const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
|
||||
return expand(acc, s).map((st) => st.join(''))
|
||||
}, [])
|
||||
|
||||
const componentVariant = expand(componentSelectors, variantSelectors).map(
|
||||
(cv) => cv.join(''),
|
||||
)
|
||||
const componentVariantStates = expand(
|
||||
componentVariant,
|
||||
statesSelectorsFlat,
|
||||
).map((cvs) => cvs.join(''))
|
||||
const selectors = expand(componentVariantStates, children).map((cvsc) =>
|
||||
cvsc.join(' '),
|
||||
)
|
||||
/*
|
||||
*/
|
||||
|
||||
if (rule.parent) {
|
||||
return genericRuleToSelector(components)(
|
||||
rule.parent,
|
||||
ignoreOutOfTreeSelector,
|
||||
liteMode,
|
||||
selectors,
|
||||
)
|
||||
}
|
||||
|
||||
return selectors.join(', ').trim()
|
||||
}
|
||||
variantSelectors = Array.isArray(variantSelectors) ? variantSelectors : [variantSelectors]
|
||||
if (ignoreOutOfTreeSelector || liteMode) variantSelectors = [variantSelectors[0]]
|
||||
|
||||
const applicableStates = (rule.state || []).filter(x => x !== 'normal')
|
||||
// const applicableStates = (rule.state || [])
|
||||
const statesSelectors = applicableStates.map(state => {
|
||||
const selector = states[state] || ''
|
||||
let arraySelector = Array.isArray(selector) ? selector : [selector]
|
||||
if (ignoreOutOfTreeSelector || liteMode) arraySelector = [arraySelector[0]]
|
||||
arraySelector
|
||||
.sort((a) => {
|
||||
if (a.startsWith(':')) return 1
|
||||
if (/^[a-z]/.exec(a)) return -1
|
||||
else return 0
|
||||
})
|
||||
.join('')
|
||||
return arraySelector
|
||||
})
|
||||
|
||||
const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
|
||||
return expand(acc, s).map(st => st.join(''))
|
||||
}, [])
|
||||
|
||||
const componentVariant = expand(componentSelectors, variantSelectors).map(cv => cv.join(''))
|
||||
const componentVariantStates = expand(componentVariant, statesSelectorsFlat).map(cvs => cvs.join(''))
|
||||
const selectors = expand(componentVariantStates, children).map(cvsc => cvsc.join(' '))
|
||||
/*
|
||||
*/
|
||||
|
||||
if (rule.parent) {
|
||||
return genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, liteMode, selectors)
|
||||
}
|
||||
|
||||
return selectors.join(', ').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if combination matches
|
||||
|
|
@ -151,8 +174,8 @@ export const combinationsMatch = (criteria, subject, strict) => {
|
|||
const criteriaStatesSet = new Set(criteria.state)
|
||||
|
||||
const setsAreEqual =
|
||||
[...criteriaStatesSet].every(state => subjectStatesSet.has(state)) &&
|
||||
[...subjectStatesSet].every(state => criteriaStatesSet.has(state))
|
||||
[...criteriaStatesSet].every((state) => subjectStatesSet.has(state)) &&
|
||||
[...subjectStatesSet].every((state) => criteriaStatesSet.has(state))
|
||||
|
||||
if (!setsAreEqual) return false
|
||||
}
|
||||
|
|
@ -168,7 +191,7 @@ export const combinationsMatch = (criteria, subject, strict) => {
|
|||
*
|
||||
* @return function that returns true/false if subject matches
|
||||
*/
|
||||
export const findRules = (criteria, strict) => subject => {
|
||||
export const findRules = (criteria, strict) => (subject) => {
|
||||
// If we searching for "general" rules - ignore "specific" ones
|
||||
if (criteria.parent === null && !!subject.parent) return false
|
||||
if (!combinationsMatch(criteria, subject, strict)) return false
|
||||
|
|
@ -186,14 +209,15 @@ export const findRules = (criteria, strict) => subject => {
|
|||
const criteriaParent = pathCriteria[i]
|
||||
const subjectParent = pathSubject[i]
|
||||
if (!subjectParent) return true
|
||||
if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false
|
||||
if (!combinationsMatch(criteriaParent, subjectParent, strict))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Pre-fills 'normal' state/variant if missing
|
||||
export const normalizeCombination = rule => {
|
||||
export const normalizeCombination = (rule) => {
|
||||
rule.variant = rule.variant ?? 'normal'
|
||||
rule.state = [...new Set(['normal', ...(rule.state || [])])]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const LAYERS = {
|
|||
alertPanel: 'panel',
|
||||
poll: 'bg',
|
||||
chatBg: 'underlay',
|
||||
chatMessage: 'chatBg'
|
||||
chatMessage: 'chatBg',
|
||||
}
|
||||
|
||||
/* By default opacity slots have 1 as default opacity
|
||||
|
|
@ -37,7 +37,7 @@ export const DEFAULT_OPACITY = {
|
|||
input: 0.5,
|
||||
faint: 0.5,
|
||||
underlay: 0.15,
|
||||
alertPopup: 0.95
|
||||
alertPopup: 0.95,
|
||||
}
|
||||
|
||||
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
|
||||
|
|
@ -82,45 +82,45 @@ export const SLOT_INHERITANCE = {
|
|||
bg: {
|
||||
depends: [],
|
||||
opacity: 'bg',
|
||||
priority: 1
|
||||
priority: 1,
|
||||
},
|
||||
wallpaper: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => brightness(-2 * mod, bg).rgb
|
||||
color: (mod, bg) => brightness(-2 * mod, bg).rgb,
|
||||
},
|
||||
fg: {
|
||||
depends: [],
|
||||
priority: 1
|
||||
priority: 1,
|
||||
},
|
||||
text: {
|
||||
depends: [],
|
||||
layer: 'bg',
|
||||
opacity: null,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
},
|
||||
underlay: {
|
||||
default: '#000000',
|
||||
opacity: 'underlay'
|
||||
opacity: 'underlay',
|
||||
},
|
||||
link: {
|
||||
depends: ['accent'],
|
||||
priority: 1
|
||||
priority: 1,
|
||||
},
|
||||
accent: {
|
||||
depends: ['link'],
|
||||
priority: 1
|
||||
priority: 1,
|
||||
},
|
||||
faint: {
|
||||
depends: ['text'],
|
||||
opacity: 'faint'
|
||||
opacity: 'faint',
|
||||
},
|
||||
faintLink: {
|
||||
depends: ['link'],
|
||||
opacity: 'faint'
|
||||
opacity: 'faint',
|
||||
},
|
||||
postFaintLink: {
|
||||
depends: ['postLink'],
|
||||
opacity: 'faint'
|
||||
opacity: 'faint',
|
||||
},
|
||||
|
||||
cBlue: '#0000ff',
|
||||
|
|
@ -133,101 +133,101 @@ export const SLOT_INHERITANCE = {
|
|||
color: (mod, bg) => ({
|
||||
r: Math.floor(bg.r * 0.53),
|
||||
g: Math.floor(bg.g * 0.56),
|
||||
b: Math.floor(bg.b * 0.59)
|
||||
})
|
||||
b: Math.floor(bg.b * 0.59),
|
||||
}),
|
||||
},
|
||||
profileTint: {
|
||||
depends: ['bg'],
|
||||
layer: 'profileTint',
|
||||
opacity: 'profileTint'
|
||||
opacity: 'profileTint',
|
||||
},
|
||||
|
||||
highlight: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb,
|
||||
},
|
||||
highlightLightText: {
|
||||
depends: ['lightText'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
highlightPostLink: {
|
||||
depends: ['postLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
highlightFaintText: {
|
||||
depends: ['faint'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
highlightFaintLink: {
|
||||
depends: ['faintLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
highlightPostFaintLink: {
|
||||
depends: ['postFaintLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
highlightText: {
|
||||
depends: ['text'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
highlightLink: {
|
||||
depends: ['link'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
highlightIcon: {
|
||||
depends: ['highlight', 'highlightText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
color: (mod, bg, text) => mixrgb(bg, text),
|
||||
},
|
||||
|
||||
popover: {
|
||||
depends: ['bg'],
|
||||
opacity: 'popover'
|
||||
opacity: 'popover',
|
||||
},
|
||||
popoverLightText: {
|
||||
depends: ['lightText'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
popoverPostLink: {
|
||||
depends: ['postLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
popoverFaintText: {
|
||||
depends: ['faint'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
popoverFaintLink: {
|
||||
depends: ['faintLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
popoverPostFaintLink: {
|
||||
depends: ['postFaintLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
popoverText: {
|
||||
depends: ['text'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
popoverLink: {
|
||||
depends: ['link'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
popoverIcon: {
|
||||
depends: ['popover', 'popoverText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
color: (mod, bg, text) => mixrgb(bg, text),
|
||||
},
|
||||
|
||||
selectedPost: '--highlight',
|
||||
|
|
@ -235,201 +235,201 @@ export const SLOT_INHERITANCE = {
|
|||
depends: ['highlightFaintText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedPostLightText: {
|
||||
depends: ['highlightLightText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedPostPostLink: {
|
||||
depends: ['highlightPostLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
selectedPostFaintLink: {
|
||||
depends: ['highlightFaintLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
selectedPostText: {
|
||||
depends: ['highlightText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedPostLink: {
|
||||
depends: ['highlightLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
selectedPostIcon: {
|
||||
depends: ['selectedPost', 'selectedPostText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
color: (mod, bg, text) => mixrgb(bg, text),
|
||||
},
|
||||
|
||||
selectedMenu: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb,
|
||||
},
|
||||
selectedMenuLightText: {
|
||||
depends: ['highlightLightText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedMenuFaintText: {
|
||||
depends: ['highlightFaintText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedMenuFaintLink: {
|
||||
depends: ['highlightFaintLink'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
selectedMenuText: {
|
||||
depends: ['highlightText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedMenuLink: {
|
||||
depends: ['highlightLink'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
selectedMenuIcon: {
|
||||
depends: ['selectedMenu', 'selectedMenuText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
color: (mod, bg, text) => mixrgb(bg, text),
|
||||
},
|
||||
|
||||
selectedMenuPopover: {
|
||||
depends: ['popover'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb,
|
||||
},
|
||||
selectedMenuPopoverLightText: {
|
||||
depends: ['selectedMenuLightText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedMenuPopoverFaintText: {
|
||||
depends: ['selectedMenuFaintText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedMenuPopoverFaintLink: {
|
||||
depends: ['selectedMenuFaintLink'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
selectedMenuPopoverText: {
|
||||
depends: ['selectedMenuText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
selectedMenuPopoverLink: {
|
||||
depends: ['selectedMenuLink'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
selectedMenuPopoverIcon: {
|
||||
depends: ['selectedMenuPopover', 'selectedMenuText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
color: (mod, bg, text) => mixrgb(bg, text),
|
||||
},
|
||||
|
||||
lightText: {
|
||||
depends: ['text'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve',
|
||||
color: (mod, text) => brightness(20 * mod, text).rgb
|
||||
color: (mod, text) => brightness(20 * mod, text).rgb,
|
||||
},
|
||||
|
||||
postLink: {
|
||||
depends: ['link'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
postGreentext: {
|
||||
depends: ['cGreen'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
postCyantext: {
|
||||
depends: ['cBlue'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
border: {
|
||||
depends: ['fg'],
|
||||
opacity: 'border',
|
||||
color: (mod, fg) => brightness(2 * mod, fg).rgb
|
||||
color: (mod, fg) => brightness(2 * mod, fg).rgb,
|
||||
},
|
||||
|
||||
poll: {
|
||||
depends: ['accent', 'bg'],
|
||||
copacity: 'poll',
|
||||
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
|
||||
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg),
|
||||
},
|
||||
pollText: {
|
||||
depends: ['text'],
|
||||
layer: 'poll',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
icon: {
|
||||
depends: ['bg', 'text'],
|
||||
inheritsOpacity: false,
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
color: (mod, bg, text) => mixrgb(bg, text),
|
||||
},
|
||||
|
||||
// Foreground
|
||||
fgText: {
|
||||
depends: ['text'],
|
||||
layer: 'fg',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
fgLink: {
|
||||
depends: ['link'],
|
||||
layer: 'fg',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
// Panel header
|
||||
panel: {
|
||||
depends: ['fg'],
|
||||
opacity: 'panel'
|
||||
opacity: 'panel',
|
||||
},
|
||||
panelText: {
|
||||
depends: ['text'],
|
||||
layer: 'panel',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
panelFaint: {
|
||||
depends: ['fgText'],
|
||||
layer: 'panel',
|
||||
opacity: 'faint',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
panelLink: {
|
||||
depends: ['fgLink'],
|
||||
layer: 'panel',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
// Top bar
|
||||
|
|
@ -437,268 +437,268 @@ export const SLOT_INHERITANCE = {
|
|||
topBarText: {
|
||||
depends: ['fgText'],
|
||||
layer: 'topBar',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
topBarLink: {
|
||||
depends: ['fgLink'],
|
||||
layer: 'topBar',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
// Tabs
|
||||
tab: {
|
||||
depends: ['btn']
|
||||
depends: ['btn'],
|
||||
},
|
||||
tabText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
tabActiveText: {
|
||||
depends: ['text'],
|
||||
layer: 'bg',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
// Buttons
|
||||
btn: {
|
||||
depends: ['fg'],
|
||||
variant: 'btn',
|
||||
opacity: 'btn'
|
||||
opacity: 'btn',
|
||||
},
|
||||
btnText: {
|
||||
depends: ['fgText'],
|
||||
layer: 'btn',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
btnPanelText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btn',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
btnTopBarText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btn',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
// Buttons: pressed
|
||||
btnPressed: {
|
||||
depends: ['btn'],
|
||||
layer: 'btn'
|
||||
layer: 'btn',
|
||||
},
|
||||
btnPressedText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
btnPressedPanel: {
|
||||
depends: ['btnPressed'],
|
||||
layer: 'btn'
|
||||
layer: 'btn',
|
||||
},
|
||||
btnPressedPanelText: {
|
||||
depends: ['btnPanelText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
btnPressedTopBar: {
|
||||
depends: ['btnPressed'],
|
||||
layer: 'btn'
|
||||
layer: 'btn',
|
||||
},
|
||||
btnPressedTopBarText: {
|
||||
depends: ['btnTopBarText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
// Buttons: toggled
|
||||
btnToggled: {
|
||||
depends: ['btn'],
|
||||
layer: 'btn',
|
||||
color: (mod, btn) => brightness(mod * 20, btn).rgb
|
||||
color: (mod, btn) => brightness(mod * 20, btn).rgb,
|
||||
},
|
||||
btnToggledText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
btnToggledPanelText: {
|
||||
depends: ['btnPanelText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
btnToggledTopBarText: {
|
||||
depends: ['btnTopBarText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
// Buttons: disabled
|
||||
btnDisabled: {
|
||||
depends: ['btn', 'bg'],
|
||||
color: (mod, btn, bg) => alphaBlend(btn, 0.25, bg)
|
||||
color: (mod, btn, bg) => alphaBlend(btn, 0.25, bg),
|
||||
},
|
||||
btnDisabledText: {
|
||||
depends: ['btnText', 'btnDisabled'],
|
||||
layer: 'btn',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn),
|
||||
},
|
||||
btnDisabledPanelText: {
|
||||
depends: ['btnPanelText', 'btnDisabled'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn),
|
||||
},
|
||||
btnDisabledTopBarText: {
|
||||
depends: ['btnTopBarText', 'btnDisabled'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn),
|
||||
},
|
||||
|
||||
// Input fields
|
||||
input: {
|
||||
depends: ['fg'],
|
||||
opacity: 'input'
|
||||
opacity: 'input',
|
||||
},
|
||||
inputText: {
|
||||
depends: ['text'],
|
||||
layer: 'input',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
inputPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'inputPanel',
|
||||
variant: 'input',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
inputTopbarText: {
|
||||
depends: ['topBarText'],
|
||||
layer: 'inputTopBar',
|
||||
variant: 'input',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertError: {
|
||||
depends: ['cRed'],
|
||||
opacity: 'alert'
|
||||
opacity: 'alert',
|
||||
},
|
||||
alertErrorText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertError',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
alertErrorPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertError',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertWarning: {
|
||||
depends: ['cOrange'],
|
||||
opacity: 'alert'
|
||||
opacity: 'alert',
|
||||
},
|
||||
alertWarningText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertWarning',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
alertWarningPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertWarning',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertSuccess: {
|
||||
depends: ['cGreen'],
|
||||
opacity: 'alert'
|
||||
opacity: 'alert',
|
||||
},
|
||||
alertSuccessText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertSuccess',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
alertSuccessPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertSuccess',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertNeutral: {
|
||||
depends: ['text'],
|
||||
opacity: 'alert'
|
||||
opacity: 'alert',
|
||||
},
|
||||
alertNeutralText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertNeutral',
|
||||
color: (mod, text) => invertLightness(text).rgb,
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
alertNeutralPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertNeutral',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertPopupError: {
|
||||
depends: ['alertError'],
|
||||
opacity: 'alertPopup'
|
||||
opacity: 'alertPopup',
|
||||
},
|
||||
alertPopupErrorText: {
|
||||
depends: ['alertErrorText'],
|
||||
layer: 'popover',
|
||||
variant: 'alertPopupError',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertPopupWarning: {
|
||||
depends: ['alertWarning'],
|
||||
opacity: 'alertPopup'
|
||||
opacity: 'alertPopup',
|
||||
},
|
||||
alertPopupWarningText: {
|
||||
depends: ['alertWarningText'],
|
||||
layer: 'popover',
|
||||
variant: 'alertPopupWarning',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertPopupSuccess: {
|
||||
depends: ['alertSuccess'],
|
||||
opacity: 'alertPopup'
|
||||
opacity: 'alertPopup',
|
||||
},
|
||||
alertPopupSuccessText: {
|
||||
depends: ['alertSuccessText'],
|
||||
layer: 'popover',
|
||||
variant: 'alertPopupSuccess',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
alertPopupNeutral: {
|
||||
depends: ['alertNeutral'],
|
||||
opacity: 'alertPopup'
|
||||
opacity: 'alertPopup',
|
||||
},
|
||||
alertPopupNeutralText: {
|
||||
depends: ['alertNeutralText'],
|
||||
layer: 'popover',
|
||||
variant: 'alertPopupNeutral',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
badgeNotification: '--cRed',
|
||||
|
|
@ -706,7 +706,7 @@ export const SLOT_INHERITANCE = {
|
|||
depends: ['text', 'badgeNotification'],
|
||||
layer: 'badge',
|
||||
variant: 'badgeNotification',
|
||||
textColor: 'bw'
|
||||
textColor: 'bw',
|
||||
},
|
||||
|
||||
badgeNeutral: '--cGreen',
|
||||
|
|
@ -714,59 +714,59 @@ export const SLOT_INHERITANCE = {
|
|||
depends: ['text', 'badgeNeutral'],
|
||||
layer: 'badge',
|
||||
variant: 'badgeNeutral',
|
||||
textColor: 'bw'
|
||||
textColor: 'bw',
|
||||
},
|
||||
|
||||
chatBg: {
|
||||
depends: ['bg']
|
||||
depends: ['bg'],
|
||||
},
|
||||
|
||||
chatMessageIncomingBg: {
|
||||
depends: ['chatBg']
|
||||
depends: ['chatBg'],
|
||||
},
|
||||
|
||||
chatMessageIncomingText: {
|
||||
depends: ['text'],
|
||||
layer: 'chatMessage',
|
||||
variant: 'chatMessageIncomingBg',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
chatMessageIncomingLink: {
|
||||
depends: ['link'],
|
||||
layer: 'chatMessage',
|
||||
variant: 'chatMessageIncomingBg',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
chatMessageIncomingBorder: {
|
||||
depends: ['border'],
|
||||
opacity: 'border',
|
||||
color: (mod, border) => brightness(2 * mod, border).rgb
|
||||
color: (mod, border) => brightness(2 * mod, border).rgb,
|
||||
},
|
||||
|
||||
chatMessageOutgoingBg: {
|
||||
depends: ['chatMessageIncomingBg'],
|
||||
color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb
|
||||
color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb,
|
||||
},
|
||||
|
||||
chatMessageOutgoingText: {
|
||||
depends: ['text'],
|
||||
layer: 'chatMessage',
|
||||
variant: 'chatMessageOutgoingBg',
|
||||
textColor: true
|
||||
textColor: true,
|
||||
},
|
||||
|
||||
chatMessageOutgoingLink: {
|
||||
depends: ['link'],
|
||||
layer: 'chatMessage',
|
||||
variant: 'chatMessageOutgoingBg',
|
||||
textColor: 'preserve'
|
||||
textColor: 'preserve',
|
||||
},
|
||||
|
||||
chatMessageOutgoingBorder: {
|
||||
depends: ['chatMessageOutgoingBg'],
|
||||
opacity: 'border',
|
||||
color: (mod, border) => brightness(2 * mod, border).rgb
|
||||
}
|
||||
color: (mod, border) => brightness(2 * mod, border).rgb,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,5 +173,5 @@ export default [
|
|||
'chatMessageOutgoingBg',
|
||||
'chatMessageOutgoingText',
|
||||
'chatMessageOutgoingLink',
|
||||
'chatMessageOutgoingBorder'
|
||||
'chatMessageOutgoingBorder',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,15 +14,10 @@ export const basePaletteKeys = new Set([
|
|||
'cGreen',
|
||||
'cOrange',
|
||||
|
||||
'wallpaper'
|
||||
'wallpaper',
|
||||
])
|
||||
|
||||
export const fontsKeys = new Set([
|
||||
'interface',
|
||||
'input',
|
||||
'post',
|
||||
'postCode'
|
||||
])
|
||||
export const fontsKeys = new Set(['interface', 'input', 'post', 'postCode'])
|
||||
|
||||
export const opacityKeys = new Set([
|
||||
'alert',
|
||||
|
|
@ -35,7 +30,7 @@ export const opacityKeys = new Set([
|
|||
'panel',
|
||||
'popover',
|
||||
'profileTint',
|
||||
'underlay'
|
||||
'underlay',
|
||||
])
|
||||
|
||||
export const shadowsKeys = new Set([
|
||||
|
|
@ -48,7 +43,7 @@ export const shadowsKeys = new Set([
|
|||
'button',
|
||||
'buttonHover',
|
||||
'buttonPressed',
|
||||
'input'
|
||||
'input',
|
||||
])
|
||||
|
||||
export const radiiKeys = new Set([
|
||||
|
|
@ -60,14 +55,11 @@ export const radiiKeys = new Set([
|
|||
'avatarAlt',
|
||||
'tooltip',
|
||||
'attachment',
|
||||
'chatMessage'
|
||||
'chatMessage',
|
||||
])
|
||||
|
||||
// Keys that are not available in editor and never meant to be edited
|
||||
export const hiddenKeys = new Set([
|
||||
'profileBg',
|
||||
'profileTint'
|
||||
])
|
||||
export const hiddenKeys = new Set(['profileBg', 'profileTint'])
|
||||
|
||||
export const extendedBasePrefixes = [
|
||||
'border',
|
||||
|
|
@ -93,33 +85,30 @@ export const extendedBasePrefixes = [
|
|||
'poll',
|
||||
|
||||
'chatBg',
|
||||
'chatMessage'
|
||||
'chatMessage',
|
||||
]
|
||||
export const nonComponentPrefixes = new Set([
|
||||
'border',
|
||||
'icon',
|
||||
'highlight',
|
||||
'lightText',
|
||||
'chatBg'
|
||||
'chatBg',
|
||||
])
|
||||
|
||||
export const extendedBaseKeys = Object.fromEntries(
|
||||
extendedBasePrefixes.map(prefix => [
|
||||
extendedBasePrefixes.map((prefix) => [
|
||||
prefix,
|
||||
allKeys.filter(k => {
|
||||
allKeys.filter((k) => {
|
||||
if (prefix === 'alert') {
|
||||
return k.startsWith(prefix) && !k.startsWith('alertPopup')
|
||||
}
|
||||
return k.startsWith(prefix)
|
||||
})
|
||||
])
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
||||
// Keysets that are only really used intermideately, i.e. to generate other colors
|
||||
export const temporary = new Set([
|
||||
'',
|
||||
'highlight'
|
||||
])
|
||||
export const temporary = new Set(['', 'highlight'])
|
||||
|
||||
export const temporaryColors = {}
|
||||
|
||||
|
|
@ -128,16 +117,18 @@ export const convertTheme2To3 = (data) => {
|
|||
data.colors.link = data.colors.link || data.colors.accent
|
||||
const generateRoot = () => {
|
||||
const directives = {}
|
||||
basePaletteKeys.forEach(key => { directives['--' + key] = 'color | ' + convert(data.colors[key]).hex })
|
||||
basePaletteKeys.forEach((key) => {
|
||||
directives['--' + key] = 'color | ' + convert(data.colors[key]).hex
|
||||
})
|
||||
return {
|
||||
component: 'Root',
|
||||
directives
|
||||
directives,
|
||||
}
|
||||
}
|
||||
|
||||
const convertOpacity = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.opacity || {}).forEach(key => {
|
||||
Object.keys(data.opacity || {}).forEach((key) => {
|
||||
if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null
|
||||
const originalOpacity = data.opacity[key]
|
||||
const rule = { source: '2to3' }
|
||||
|
|
@ -201,7 +192,12 @@ export const convertTheme2To3 = (data) => {
|
|||
if (rule.component === 'Button') {
|
||||
newRules.push({ ...rule, component: 'ScrollbarElement' })
|
||||
newRules.push({ ...rule, component: 'Tab' })
|
||||
newRules.push({ ...rule, component: 'Tab', state: ['active'], directives: { opacity: 0 } })
|
||||
newRules.push({
|
||||
...rule,
|
||||
component: 'Tab',
|
||||
state: ['active'],
|
||||
directives: { opacity: 0 },
|
||||
})
|
||||
}
|
||||
if (rule.component === 'Panel') {
|
||||
newRules.push({ ...rule, component: 'Post' })
|
||||
|
|
@ -212,7 +208,7 @@ export const convertTheme2To3 = (data) => {
|
|||
|
||||
const convertRadii = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.radii || {}).forEach(key => {
|
||||
Object.keys(data.radii || {}).forEach((key) => {
|
||||
if (!radiiKeys.has(key) || data.radii[key] === undefined) return null
|
||||
const originalRadius = data.radii[key]
|
||||
const rule = { source: '2to3' }
|
||||
|
|
@ -249,7 +245,7 @@ export const convertTheme2To3 = (data) => {
|
|||
break
|
||||
}
|
||||
rule.directives = {
|
||||
roundness: originalRadius
|
||||
roundness: originalRadius,
|
||||
}
|
||||
newRules.push(rule)
|
||||
if (rule.component === 'Button') {
|
||||
|
|
@ -262,7 +258,7 @@ export const convertTheme2To3 = (data) => {
|
|||
|
||||
const convertFonts = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.fonts || {}).forEach(key => {
|
||||
Object.keys(data.fonts || {}).forEach((key) => {
|
||||
if (!fontsKeys.has(key)) return
|
||||
if (!data.fonts[key]) return
|
||||
const originalFont = data.fonts[key].family
|
||||
|
|
@ -297,7 +293,7 @@ export const convertTheme2To3 = (data) => {
|
|||
}
|
||||
const convertShadows = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.shadows || {}).forEach(key => {
|
||||
Object.keys(data.shadows || {}).forEach((key) => {
|
||||
if (!shadowsKeys.has(key)) return
|
||||
const originalShadow = data.shadows[key]
|
||||
const rule = { source: '2to3' }
|
||||
|
|
@ -338,11 +334,15 @@ export const convertTheme2To3 = (data) => {
|
|||
break
|
||||
}
|
||||
rule.directives = {
|
||||
shadow: originalShadow
|
||||
shadow: originalShadow,
|
||||
}
|
||||
newRules.push(rule)
|
||||
if (key === 'topBar') {
|
||||
newRules.push({ ...rule, component: 'PanelHeader', parent: { component: 'MobileDrawer' } })
|
||||
newRules.push({
|
||||
...rule,
|
||||
component: 'PanelHeader',
|
||||
parent: { component: 'MobileDrawer' },
|
||||
})
|
||||
}
|
||||
if (key === 'avatarStatus') {
|
||||
newRules.push({ ...rule, parent: { component: 'Notification' } })
|
||||
|
|
@ -363,169 +363,211 @@ export const convertTheme2To3 = (data) => {
|
|||
return newRules
|
||||
}
|
||||
|
||||
const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => {
|
||||
if (nonComponentPrefixes.has(prefix)) return null
|
||||
const rule = { source: '2to3' }
|
||||
if (prefix === 'alertPopup') {
|
||||
rule.component = 'Alert'
|
||||
rule.parent = { component: 'Popover' }
|
||||
} else if (prefix === 'selectedPost') {
|
||||
rule.component = 'Post'
|
||||
rule.state = ['selected']
|
||||
} else if (prefix === 'selectedMenu') {
|
||||
rule.component = 'MenuItem'
|
||||
rule.state = ['hover']
|
||||
} else if (prefix === 'chatMessageIncoming') {
|
||||
rule.component = 'ChatMessage'
|
||||
} else if (prefix === 'chatMessageOutgoing') {
|
||||
rule.component = 'ChatMessage'
|
||||
rule.variant = 'outgoing'
|
||||
} else if (prefix === 'panel') {
|
||||
rule.component = 'PanelHeader'
|
||||
} else if (prefix === 'topBar') {
|
||||
rule.component = 'TopBar'
|
||||
} else if (prefix === 'chatMessage') {
|
||||
rule.component = 'ChatMessage'
|
||||
} else if (prefix === 'poll') {
|
||||
rule.component = 'PollGraph'
|
||||
} else if (prefix === 'btn') {
|
||||
rule.component = 'Button'
|
||||
} else {
|
||||
rule.component = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase()
|
||||
}
|
||||
return keys.map((key) => {
|
||||
if (!data.colors[key]) return null
|
||||
const leftoverKey = key.replace(prefix, '')
|
||||
const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g)
|
||||
const last = parts.slice(-1)[0]
|
||||
let newRule = { source: '2to3', directives: {} }
|
||||
let variantArray = []
|
||||
|
||||
switch (last) {
|
||||
case 'Text':
|
||||
case 'Faint': // typo
|
||||
case 'Link':
|
||||
case 'Icon':
|
||||
case 'Greentext':
|
||||
case 'Cyantext':
|
||||
case 'Border':
|
||||
newRule.parent = rule
|
||||
newRule.directives.textColor = data.colors[key]
|
||||
variantArray = parts.slice(0, -1)
|
||||
break
|
||||
default:
|
||||
newRule = { ...rule, directives: {} }
|
||||
newRule.directives.background = data.colors[key]
|
||||
variantArray = parts
|
||||
break
|
||||
const extendedRules = Object.entries(extendedBaseKeys).map(
|
||||
([prefix, keys]) => {
|
||||
if (nonComponentPrefixes.has(prefix)) return null
|
||||
const rule = { source: '2to3' }
|
||||
if (prefix === 'alertPopup') {
|
||||
rule.component = 'Alert'
|
||||
rule.parent = { component: 'Popover' }
|
||||
} else if (prefix === 'selectedPost') {
|
||||
rule.component = 'Post'
|
||||
rule.state = ['selected']
|
||||
} else if (prefix === 'selectedMenu') {
|
||||
rule.component = 'MenuItem'
|
||||
rule.state = ['hover']
|
||||
} else if (prefix === 'chatMessageIncoming') {
|
||||
rule.component = 'ChatMessage'
|
||||
} else if (prefix === 'chatMessageOutgoing') {
|
||||
rule.component = 'ChatMessage'
|
||||
rule.variant = 'outgoing'
|
||||
} else if (prefix === 'panel') {
|
||||
rule.component = 'PanelHeader'
|
||||
} else if (prefix === 'topBar') {
|
||||
rule.component = 'TopBar'
|
||||
} else if (prefix === 'chatMessage') {
|
||||
rule.component = 'ChatMessage'
|
||||
} else if (prefix === 'poll') {
|
||||
rule.component = 'PollGraph'
|
||||
} else if (prefix === 'btn') {
|
||||
rule.component = 'Button'
|
||||
} else {
|
||||
rule.component = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase()
|
||||
}
|
||||
return keys.map((key) => {
|
||||
if (!data.colors[key]) return null
|
||||
const leftoverKey = key.replace(prefix, '')
|
||||
const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g)
|
||||
const last = parts.slice(-1)[0]
|
||||
let newRule = { source: '2to3', directives: {} }
|
||||
let variantArray = []
|
||||
|
||||
if (last === 'Text' || last === 'Link') {
|
||||
const secondLast = parts.slice(-2)[0]
|
||||
if (secondLast === 'Light') {
|
||||
return null // unsupported
|
||||
} else if (secondLast === 'Faint') {
|
||||
newRule.state = ['faint']
|
||||
variantArray = parts.slice(0, -2)
|
||||
switch (last) {
|
||||
case 'Text':
|
||||
case 'Faint': // typo
|
||||
case 'Link':
|
||||
case 'Icon':
|
||||
case 'Greentext':
|
||||
case 'Cyantext':
|
||||
case 'Border':
|
||||
newRule.parent = rule
|
||||
newRule.directives.textColor = data.colors[key]
|
||||
variantArray = parts.slice(0, -1)
|
||||
break
|
||||
default:
|
||||
newRule = { ...rule, directives: {} }
|
||||
newRule.directives.background = data.colors[key]
|
||||
variantArray = parts
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch (last) {
|
||||
case 'Text':
|
||||
case 'Link':
|
||||
case 'Icon':
|
||||
case 'Border':
|
||||
newRule.component = last
|
||||
break
|
||||
case 'Greentext':
|
||||
case 'Cyantext':
|
||||
newRule.component = 'FunText'
|
||||
newRule.variant = last.toLowerCase()
|
||||
break
|
||||
case 'Faint':
|
||||
newRule.component = 'Text'
|
||||
newRule.state = ['faint']
|
||||
break
|
||||
}
|
||||
|
||||
variantArray = variantArray.filter(x => x !== 'Bg')
|
||||
|
||||
if (last === 'Link' && prefix === 'selectedPost') {
|
||||
// selectedPost has typo - duplicate 'Post'
|
||||
variantArray = variantArray.filter(x => x !== 'Post')
|
||||
}
|
||||
|
||||
if (prefix === 'popover' && variantArray[0] === 'Post') {
|
||||
newRule.component = 'Post'
|
||||
newRule.parent = { source: '2to3hack', component: 'Popover' }
|
||||
variantArray = variantArray.filter(x => x !== 'Post')
|
||||
}
|
||||
|
||||
if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') {
|
||||
newRule.parent = { source: '2to3hack', component: 'Popover' }
|
||||
variantArray = variantArray.filter(x => x !== 'Popover')
|
||||
}
|
||||
|
||||
switch (prefix) {
|
||||
case 'btn':
|
||||
case 'input':
|
||||
case 'alert': {
|
||||
const hasPanel = variantArray.find(x => x === 'Panel')
|
||||
if (hasPanel) {
|
||||
newRule.parent = { source: '2to3hack', component: 'PanelHeader', parent: newRule.parent }
|
||||
variantArray = variantArray.filter(x => x !== 'Panel')
|
||||
if (last === 'Text' || last === 'Link') {
|
||||
const secondLast = parts.slice(-2)[0]
|
||||
if (secondLast === 'Light') {
|
||||
return null // unsupported
|
||||
} else if (secondLast === 'Faint') {
|
||||
newRule.state = ['faint']
|
||||
variantArray = parts.slice(0, -2)
|
||||
}
|
||||
const hasTop = variantArray.find(x => x === 'Top') // TopBar
|
||||
if (hasTop) {
|
||||
newRule.parent = { source: '2to3hack', component: 'TopBar', parent: newRule.parent }
|
||||
variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar')
|
||||
}
|
||||
|
||||
switch (last) {
|
||||
case 'Text':
|
||||
case 'Link':
|
||||
case 'Icon':
|
||||
case 'Border':
|
||||
newRule.component = last
|
||||
break
|
||||
case 'Greentext':
|
||||
case 'Cyantext':
|
||||
newRule.component = 'FunText'
|
||||
newRule.variant = last.toLowerCase()
|
||||
break
|
||||
case 'Faint':
|
||||
newRule.component = 'Text'
|
||||
newRule.state = ['faint']
|
||||
break
|
||||
}
|
||||
|
||||
variantArray = variantArray.filter((x) => x !== 'Bg')
|
||||
|
||||
if (last === 'Link' && prefix === 'selectedPost') {
|
||||
// selectedPost has typo - duplicate 'Post'
|
||||
variantArray = variantArray.filter((x) => x !== 'Post')
|
||||
}
|
||||
|
||||
if (prefix === 'popover' && variantArray[0] === 'Post') {
|
||||
newRule.component = 'Post'
|
||||
newRule.parent = { source: '2to3hack', component: 'Popover' }
|
||||
variantArray = variantArray.filter((x) => x !== 'Post')
|
||||
}
|
||||
|
||||
if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') {
|
||||
newRule.parent = { source: '2to3hack', component: 'Popover' }
|
||||
variantArray = variantArray.filter((x) => x !== 'Popover')
|
||||
}
|
||||
|
||||
switch (prefix) {
|
||||
case 'btn':
|
||||
case 'input':
|
||||
case 'alert': {
|
||||
const hasPanel = variantArray.find((x) => x === 'Panel')
|
||||
if (hasPanel) {
|
||||
newRule.parent = {
|
||||
source: '2to3hack',
|
||||
component: 'PanelHeader',
|
||||
parent: newRule.parent,
|
||||
}
|
||||
variantArray = variantArray.filter((x) => x !== 'Panel')
|
||||
}
|
||||
const hasTop = variantArray.find((x) => x === 'Top') // TopBar
|
||||
if (hasTop) {
|
||||
newRule.parent = {
|
||||
source: '2to3hack',
|
||||
component: 'TopBar',
|
||||
parent: newRule.parent,
|
||||
}
|
||||
variantArray = variantArray.filter(
|
||||
(x) => x !== 'Top' && x !== 'Bar',
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (variantArray.length > 0) {
|
||||
if (prefix === 'btn') {
|
||||
newRule.state = variantArray.map(x => x.toLowerCase())
|
||||
} else {
|
||||
newRule.variant = variantArray[0].toLowerCase()
|
||||
if (variantArray.length > 0) {
|
||||
if (prefix === 'btn') {
|
||||
newRule.state = variantArray.map((x) => x.toLowerCase())
|
||||
} else {
|
||||
newRule.variant = variantArray[0].toLowerCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.component === 'Panel') {
|
||||
return [newRule, { ...newRule, component: 'MobileDrawer' }]
|
||||
} else if (newRule.component === 'Button') {
|
||||
const rules = [
|
||||
newRule,
|
||||
{ ...newRule, component: 'Tab' },
|
||||
{ ...newRule, component: 'ScrollbarElement' }
|
||||
]
|
||||
if (newRule.state?.indexOf('toggled') >= 0) {
|
||||
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
|
||||
rules.push({ ...newRule, state: [...newRule.state, 'hover'] })
|
||||
rules.push({ ...newRule, state: [...newRule.state, 'hover', 'focused'] })
|
||||
}
|
||||
if (newRule.state?.indexOf('hover') >= 0) {
|
||||
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
|
||||
}
|
||||
return rules
|
||||
} else if (newRule.component === 'Badge') {
|
||||
if (newRule.variant === 'notification') {
|
||||
return [newRule, { component: 'Root', directives: { '--badgeNotification': 'color | ' + newRule.directives.background } }]
|
||||
} else if (newRule.variant === 'neutral') {
|
||||
return [{ ...newRule, variant: 'normal' }]
|
||||
if (newRule.component === 'Panel') {
|
||||
return [newRule, { ...newRule, component: 'MobileDrawer' }]
|
||||
} else if (newRule.component === 'Button') {
|
||||
const rules = [
|
||||
newRule,
|
||||
{ ...newRule, component: 'Tab' },
|
||||
{ ...newRule, component: 'ScrollbarElement' },
|
||||
]
|
||||
if (newRule.state?.indexOf('toggled') >= 0) {
|
||||
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
|
||||
rules.push({ ...newRule, state: [...newRule.state, 'hover'] })
|
||||
rules.push({
|
||||
...newRule,
|
||||
state: [...newRule.state, 'hover', 'focused'],
|
||||
})
|
||||
}
|
||||
if (newRule.state?.indexOf('hover') >= 0) {
|
||||
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
|
||||
}
|
||||
return rules
|
||||
} else if (newRule.component === 'Badge') {
|
||||
if (newRule.variant === 'notification') {
|
||||
return [
|
||||
newRule,
|
||||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--badgeNotification':
|
||||
'color | ' + newRule.directives.background,
|
||||
},
|
||||
},
|
||||
]
|
||||
} else if (newRule.variant === 'neutral') {
|
||||
return [{ ...newRule, variant: 'normal' }]
|
||||
} else {
|
||||
return [newRule]
|
||||
}
|
||||
} else if (newRule.component === 'TopBar') {
|
||||
return [
|
||||
newRule,
|
||||
{
|
||||
...newRule,
|
||||
parent: { component: 'MobileDrawer' },
|
||||
component: 'PanelHeader',
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return [newRule]
|
||||
}
|
||||
} else if (newRule.component === 'TopBar') {
|
||||
return [newRule, { ...newRule, parent: { component: 'MobileDrawer' }, component: 'PanelHeader' }]
|
||||
} else {
|
||||
return [newRule]
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const flatExtRules = extendedRules.filter(x => x).reduce((acc, x) => [...acc, ...x], []).filter(x => x).reduce((acc, x) => [...acc, ...x], [])
|
||||
const flatExtRules = extendedRules
|
||||
.filter((x) => x)
|
||||
.reduce((acc, x) => [...acc, ...x], [])
|
||||
.filter((x) => x)
|
||||
.reduce((acc, x) => [...acc, ...x], [])
|
||||
|
||||
return [generateRoot(), ...convertShadows(), ...convertRadii(), ...convertOpacity(), ...convertFonts(), ...flatExtRules]
|
||||
return [
|
||||
generateRoot(),
|
||||
...convertShadows(),
|
||||
...convertRadii(),
|
||||
...convertOpacity(),
|
||||
...convertFonts(),
|
||||
...flatExtRules,
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,28 @@
|
|||
import { convert, brightness } from 'chromatism'
|
||||
import { alphaBlend, arithmeticBlend, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
|
||||
import {
|
||||
alphaBlend,
|
||||
arithmeticBlend,
|
||||
getTextColor,
|
||||
relativeLuminance,
|
||||
} from '../color_convert/color_convert.js'
|
||||
|
||||
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
|
||||
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-+,.'"\s]*)\)/.exec(text).groups
|
||||
const args = argsString.split(/ /g).map(a => a.trim())
|
||||
export const process = (
|
||||
text,
|
||||
functions,
|
||||
{ findColor, findShadow },
|
||||
{ dynamicVars, staticVars },
|
||||
) => {
|
||||
const { funcName, argsString } =
|
||||
/\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-+,.'"\s]*)\)/.exec(
|
||||
text,
|
||||
).groups
|
||||
const args = argsString.split(/ /g).map((a) => a.trim())
|
||||
|
||||
const func = functions[funcName]
|
||||
if (args.length < func.argsNeeded) {
|
||||
throw new Error(`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`)
|
||||
throw new Error(
|
||||
`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`,
|
||||
)
|
||||
}
|
||||
return func.exec(args, { findColor, findShadow }, { dynamicVars, staticVars })
|
||||
}
|
||||
|
|
@ -15,53 +30,57 @@ export const process = (text, functions, { findColor, findShadow }, { dynamicVar
|
|||
export const colorFunctions = {
|
||||
alpha: {
|
||||
argsNeeded: 2,
|
||||
documentation: 'Changes alpha value of the color only to be used for CSS variables',
|
||||
args: [
|
||||
'color: source color used',
|
||||
'amount: alpha value'
|
||||
],
|
||||
documentation:
|
||||
'Changes alpha value of the color only to be used for CSS variables',
|
||||
args: ['color: source color used', 'amount: alpha value'],
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [color, amountArg] = args
|
||||
|
||||
const colorArg = convert(findColor(color, { dynamicVars, staticVars })).rgb
|
||||
const colorArg = convert(
|
||||
findColor(color, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const amount = Number(amountArg)
|
||||
return { ...colorArg, a: amount }
|
||||
}
|
||||
},
|
||||
},
|
||||
brightness: {
|
||||
argsNeeded: 2,
|
||||
document: 'Changes brightness/lightness of color in HSL colorspace',
|
||||
args: [
|
||||
'color: source color used',
|
||||
'amount: lightness value'
|
||||
],
|
||||
args: ['color: source color used', 'amount: lightness value'],
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [color, amountArg] = args
|
||||
|
||||
const colorArg = convert(findColor(color, { dynamicVars, staticVars })).hsl
|
||||
const colorArg = convert(
|
||||
findColor(color, { dynamicVars, staticVars }),
|
||||
).hsl
|
||||
colorArg.l += Number(amountArg)
|
||||
return { ...convert(colorArg).rgb }
|
||||
}
|
||||
},
|
||||
},
|
||||
textColor: {
|
||||
argsNeeded: 2,
|
||||
documentation: 'Get text color with adequate contrast for given background and intended text color. Same function is used internally',
|
||||
documentation:
|
||||
'Get text color with adequate contrast for given background and intended text color. Same function is used internally',
|
||||
args: [
|
||||
'background: color of backdrop where text will be shown',
|
||||
'foreground: intended text color',
|
||||
`[preserve]: (optional) intended color preservation:
|
||||
'preserve' - try to preserve the color
|
||||
'no-preserve' - if can't get adequate color - fall back to black or white
|
||||
'no-auto' - don't do anything (useless as a color function)`
|
||||
'no-auto' - don't do anything (useless as a color function)`,
|
||||
],
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [backgroundArg, foregroundArg, preserve = 'preserve'] = args
|
||||
|
||||
const background = convert(findColor(backgroundArg, { dynamicVars, staticVars })).rgb
|
||||
const foreground = convert(findColor(foregroundArg, { dynamicVars, staticVars })).rgb
|
||||
const background = convert(
|
||||
findColor(backgroundArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const foreground = convert(
|
||||
findColor(foregroundArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
|
||||
return getTextColor(background, foreground, preserve === 'preserve')
|
||||
}
|
||||
},
|
||||
},
|
||||
blend: {
|
||||
argsNeeded: 3,
|
||||
|
|
@ -69,17 +88,21 @@ export const colorFunctions = {
|
|||
args: [
|
||||
'background: bottom layer color',
|
||||
'amount: opacity of top layer',
|
||||
'foreground: upper layer color'
|
||||
'foreground: upper layer color',
|
||||
],
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [backgroundArg, amountArg, foregroundArg] = args
|
||||
|
||||
const background = convert(findColor(backgroundArg, { dynamicVars, staticVars })).rgb
|
||||
const foreground = convert(findColor(foregroundArg, { dynamicVars, staticVars })).rgb
|
||||
const background = convert(
|
||||
findColor(backgroundArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const foreground = convert(
|
||||
findColor(foregroundArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const amount = Number(amountArg)
|
||||
|
||||
return alphaBlend(background, amount, foreground)
|
||||
}
|
||||
},
|
||||
},
|
||||
shift: {
|
||||
argsNeeded: 2,
|
||||
|
|
@ -87,66 +110,72 @@ export const colorFunctions = {
|
|||
args: [
|
||||
'origin: base color',
|
||||
'value: shift value',
|
||||
'operator: math operator to use (+ or -)'
|
||||
'operator: math operator to use (+ or -)',
|
||||
],
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [originArg, valueArg, operatorArg] = args
|
||||
|
||||
const origin = convert(findColor(originArg, { dynamicVars, staticVars })).rgb
|
||||
const value = convert(findColor(valueArg, { dynamicVars, staticVars })).rgb
|
||||
const origin = convert(
|
||||
findColor(originArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const value = convert(
|
||||
findColor(valueArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
|
||||
return arithmeticBlend(origin, value, operatorArg)
|
||||
}
|
||||
},
|
||||
},
|
||||
boost: {
|
||||
argsNeeded: 2,
|
||||
documentation: 'If given color is dark makes it darker, if color is light - makes it lighter',
|
||||
args: [
|
||||
'color: source color',
|
||||
'amount: how much darken/brighten the color'
|
||||
],
|
||||
documentation:
|
||||
'If given color is dark makes it darker, if color is light - makes it lighter',
|
||||
args: ['color: source color', 'amount: how much darken/brighten the color'],
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [colorArg, amountArg] = args
|
||||
|
||||
const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb
|
||||
const color = convert(
|
||||
findColor(colorArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const amount = Number(amountArg)
|
||||
|
||||
const isLight = relativeLuminance(color) < 0.5
|
||||
const mod = isLight ? -1 : 1
|
||||
return brightness(amount * mod, color).rgb
|
||||
}
|
||||
},
|
||||
},
|
||||
mod: {
|
||||
argsNeeded: 2,
|
||||
documentation: 'Old function that increases or decreases brightness depending if background color is dark or light. Advised against using it as it might give unexpected results.',
|
||||
args: [
|
||||
'color: source color',
|
||||
'amount: how much darken/brighten the color'
|
||||
],
|
||||
documentation:
|
||||
'Old function that increases or decreases brightness depending if background color is dark or light. Advised against using it as it might give unexpected results.',
|
||||
args: ['color: source color', 'amount: how much darken/brighten the color'],
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [colorArg, amountArg] = args
|
||||
|
||||
const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb
|
||||
const color = convert(
|
||||
findColor(colorArg, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const amount = Number(amountArg)
|
||||
|
||||
const effectiveBackground = dynamicVars.lowerLevelBackground ?? color
|
||||
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
||||
const isLightOnDark =
|
||||
relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
return brightness(amount * mod, color).rgb
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const shadowFunctions = {
|
||||
borderSide: {
|
||||
argsNeeded: 3,
|
||||
documentation: 'Simulate a border on a side with a shadow, best works on inset border',
|
||||
documentation:
|
||||
'Simulate a border on a side with a shadow, best works on inset border',
|
||||
args: [
|
||||
'color: border color',
|
||||
'side: string indicating on which side border should be, takes either one word or two words joined by dash (i.e. "left" or "bottom-right")',
|
||||
'width: border width (thickness)',
|
||||
'[alpha]: (Optional) border opacity, defaults to 1 (fully opaque)',
|
||||
'[inset]: (Optional) whether border should be on the inside or outside, defaults to inside'
|
||||
'[inset]: (Optional) whether border should be on the inside or outside, defaults to inside',
|
||||
],
|
||||
exec: (args) => {
|
||||
const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args
|
||||
|
|
@ -161,7 +190,7 @@ export const shadowFunctions = {
|
|||
spread: 0,
|
||||
color,
|
||||
alpha: Number(alpha),
|
||||
inset: isInset
|
||||
inset: isInset,
|
||||
}
|
||||
|
||||
side.split('-').forEach((position) => {
|
||||
|
|
@ -181,6 +210,6 @@ export const shadowFunctions = {
|
|||
}
|
||||
})
|
||||
return [targetShadow]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { convert, brightness, contrastRatio } from 'chromatism'
|
||||
import { rgb2hex, rgba2css, alphaBlendLayers, getTextColor, relativeLuminance, getCssColor } from '../color_convert/color_convert.js'
|
||||
import {
|
||||
rgb2hex,
|
||||
rgba2css,
|
||||
alphaBlendLayers,
|
||||
getTextColor,
|
||||
relativeLuminance,
|
||||
getCssColor,
|
||||
} from '../color_convert/color_convert.js'
|
||||
import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
|
||||
|
||||
/*
|
||||
|
|
@ -48,15 +55,17 @@ export const getLayersArray = (layer, data = LAYERS) => {
|
|||
return array
|
||||
}
|
||||
|
||||
export const getLayers = (layer, variant = layer, opacitySlot, colors, opacity) => {
|
||||
return getLayersArray(layer).map((currentLayer) => ([
|
||||
currentLayer === layer
|
||||
? colors[variant]
|
||||
: colors[currentLayer],
|
||||
currentLayer === layer
|
||||
? opacity[opacitySlot] || 1
|
||||
: opacity[currentLayer]
|
||||
]))
|
||||
export const getLayers = (
|
||||
layer,
|
||||
variant = layer,
|
||||
opacitySlot,
|
||||
colors,
|
||||
opacity,
|
||||
) => {
|
||||
return getLayersArray(layer).map((currentLayer) => [
|
||||
currentLayer === layer ? colors[variant] : colors[currentLayer],
|
||||
currentLayer === layer ? opacity[opacitySlot] || 1 : opacity[currentLayer],
|
||||
])
|
||||
}
|
||||
|
||||
const getDependencies = (key, inheritance) => {
|
||||
|
|
@ -67,11 +76,9 @@ const getDependencies = (key, inheritance) => {
|
|||
if (data === null) return []
|
||||
const { depends, layer, variant } = data
|
||||
const layerDeps = layer
|
||||
? getLayersArray(layer).map(currentLayer => {
|
||||
return currentLayer === layer
|
||||
? variant || layer
|
||||
: currentLayer
|
||||
})
|
||||
? getLayersArray(layer).map((currentLayer) => {
|
||||
return currentLayer === layer ? variant || layer : currentLayer
|
||||
})
|
||||
: []
|
||||
if (Array.isArray(depends)) {
|
||||
return [...depends, ...layerDeps]
|
||||
|
|
@ -93,7 +100,7 @@ const getDependencies = (key, inheritance) => {
|
|||
*/
|
||||
export const topoSort = (
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
getDeps = getDependencies,
|
||||
) => {
|
||||
// This is an implementation of https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
|
||||
|
||||
|
|
@ -130,22 +137,25 @@ export const topoSort = (
|
|||
|
||||
// The index thing is to make sorting stable on browsers
|
||||
// where Array.sort() isn't stable
|
||||
return output.map((data, index) => ({ data, index })).sort(({ data: a, index: ai }, { data: b, index: bi }) => {
|
||||
const depsA = getDeps(a, inheritance).length
|
||||
const depsB = getDeps(b, inheritance).length
|
||||
return output
|
||||
.map((data, index) => ({ data, index }))
|
||||
.sort(({ data: a, index: ai }, { data: b, index: bi }) => {
|
||||
const depsA = getDeps(a, inheritance).length
|
||||
const depsB = getDeps(b, inheritance).length
|
||||
|
||||
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return ai - bi
|
||||
if (depsA === 0 && depsB !== 0) return -1
|
||||
if (depsB === 0 && depsA !== 0) return 1
|
||||
return 0 // failsafe, shouldn't happen?
|
||||
}).map(({ data }) => data)
|
||||
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return ai - bi
|
||||
if (depsA === 0 && depsB !== 0) return -1
|
||||
if (depsB === 0 && depsA !== 0) return 1
|
||||
return 0 // failsafe, shouldn't happen?
|
||||
})
|
||||
.map(({ data }) => data)
|
||||
}
|
||||
|
||||
const expandSlotValue = (value) => {
|
||||
if (typeof value === 'object') return value
|
||||
return {
|
||||
depends: value.startsWith('--') ? [value.substring(2)] : [],
|
||||
default: value.startsWith('#') ? value : undefined
|
||||
default: value.startsWith('#') ? value : undefined,
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -156,7 +166,7 @@ const expandSlotValue = (value) => {
|
|||
export const getOpacitySlot = (
|
||||
k,
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
getDeps = getDependencies,
|
||||
) => {
|
||||
const value = expandSlotValue(inheritance[k])
|
||||
if (value.opacity === null) return
|
||||
|
|
@ -189,7 +199,7 @@ export const getOpacitySlot = (
|
|||
export const getLayerSlot = (
|
||||
k,
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
getDeps = getDependencies,
|
||||
) => {
|
||||
const value = expandSlotValue(inheritance[k])
|
||||
if (LAYERS[k]) return k
|
||||
|
|
@ -218,8 +228,11 @@ export const getLayerSlot = (
|
|||
*/
|
||||
export const SLOT_ORDERED = topoSort(
|
||||
Object.entries(SLOT_INHERITANCE)
|
||||
.sort(([, aV], [, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
|
||||
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
|
||||
.sort(
|
||||
([, aV], [, bV]) =>
|
||||
((aV && aV.priority) || 0) - ((bV && bV.priority) || 0),
|
||||
)
|
||||
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}),
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -233,8 +246,11 @@ export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k]) => {
|
|||
...acc,
|
||||
[opacity]: {
|
||||
defaultValue: DEFAULT_OPACITY[opacity] || 1,
|
||||
affectedSlots: [...((acc[opacity] && acc[opacity].affectedSlots) || []), k]
|
||||
}
|
||||
affectedSlots: [
|
||||
...((acc[opacity] && acc[opacity].affectedSlots) || []),
|
||||
k,
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
|
|
@ -245,10 +261,11 @@ export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k]) => {
|
|||
* Handle dynamic color
|
||||
*/
|
||||
export const computeDynamicColor = (sourceColor, getColor, mod) => {
|
||||
if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--')) return sourceColor
|
||||
if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--'))
|
||||
return sourceColor
|
||||
let targetColor = null
|
||||
// Color references other color
|
||||
const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
|
||||
const [variable, modifier] = sourceColor.split(/,/g).map((str) => str.trim())
|
||||
const variableSlot = variable.substring(2)
|
||||
targetColor = getColor(variableSlot)
|
||||
if (modifier) {
|
||||
|
|
@ -261,151 +278,167 @@ export const computeDynamicColor = (sourceColor, getColor, mod) => {
|
|||
* THE function you want to use. Takes provided colors and opacities
|
||||
* value and uses inheritance data to figure out color needed for the slot.
|
||||
*/
|
||||
export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
|
||||
const sourceColor = sourceColors[key]
|
||||
const value = expandSlotValue(SLOT_INHERITANCE[key])
|
||||
const deps = getDependencies(key, SLOT_INHERITANCE)
|
||||
const isTextColor = !!value.textColor
|
||||
const variant = value.variant || value.layer
|
||||
export const getColors = (sourceColors, sourceOpacity) =>
|
||||
SLOT_ORDERED.reduce(
|
||||
({ colors, opacity }, key) => {
|
||||
const sourceColor = sourceColors[key]
|
||||
const value = expandSlotValue(SLOT_INHERITANCE[key])
|
||||
const deps = getDependencies(key, SLOT_INHERITANCE)
|
||||
const isTextColor = !!value.textColor
|
||||
const variant = value.variant || value.layer
|
||||
|
||||
let backgroundColor = null
|
||||
let backgroundColor = null
|
||||
|
||||
if (isTextColor) {
|
||||
backgroundColor = alphaBlendLayers(
|
||||
{ ...(colors[deps[0]] || convert(sourceColors[key] || '#FF00FF').rgb) },
|
||||
getLayers(
|
||||
getLayerSlot(key) || 'bg',
|
||||
variant || 'bg',
|
||||
getOpacitySlot(variant),
|
||||
colors,
|
||||
opacity
|
||||
)
|
||||
)
|
||||
} else if (variant && variant !== key) {
|
||||
backgroundColor = colors[variant] || convert(sourceColors[variant]).rgb
|
||||
} else {
|
||||
backgroundColor = colors.bg || convert(sourceColors.bg)
|
||||
}
|
||||
|
||||
const isLightOnDark = relativeLuminance(backgroundColor) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
|
||||
let outputColor = null
|
||||
if (sourceColor) {
|
||||
// Color is defined in source color
|
||||
let targetColor = sourceColor
|
||||
if (targetColor === 'transparent') {
|
||||
// We take only layers below current one
|
||||
const layers = getLayers(
|
||||
getLayerSlot(key),
|
||||
key,
|
||||
getOpacitySlot(key) || key,
|
||||
colors,
|
||||
opacity
|
||||
).slice(0, -1)
|
||||
targetColor = {
|
||||
...alphaBlendLayers(
|
||||
convert('#FF00FF').rgb,
|
||||
layers
|
||||
),
|
||||
a: 0
|
||||
}
|
||||
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
|
||||
targetColor = computeDynamicColor(
|
||||
sourceColor,
|
||||
variableSlot => colors[variableSlot] || sourceColors[variableSlot],
|
||||
mod
|
||||
)
|
||||
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) {
|
||||
targetColor = convert(targetColor).rgb
|
||||
}
|
||||
outputColor = { ...targetColor }
|
||||
} else if (value.default) {
|
||||
// same as above except in object form
|
||||
outputColor = convert(value.default).rgb
|
||||
} else {
|
||||
// calculate color
|
||||
const defaultColorFunc = (mod, dep) => ({ ...dep })
|
||||
const colorFunc = value.color || defaultColorFunc
|
||||
|
||||
if (value.textColor) {
|
||||
if (value.textColor === 'bw') {
|
||||
outputColor = contrastRatio(backgroundColor).rgb
|
||||
} else {
|
||||
let color = { ...colors[deps[0]] }
|
||||
if (value.color) {
|
||||
color = colorFunc(mod, ...deps.map((dep) => ({ ...colors[dep] })))
|
||||
}
|
||||
outputColor = getTextColor(
|
||||
backgroundColor,
|
||||
{ ...color },
|
||||
value.textColor === 'preserve'
|
||||
if (isTextColor) {
|
||||
backgroundColor = alphaBlendLayers(
|
||||
{
|
||||
...(colors[deps[0]] || convert(sourceColors[key] || '#FF00FF').rgb),
|
||||
},
|
||||
getLayers(
|
||||
getLayerSlot(key) || 'bg',
|
||||
variant || 'bg',
|
||||
getOpacitySlot(variant),
|
||||
colors,
|
||||
opacity,
|
||||
),
|
||||
)
|
||||
} else if (variant && variant !== key) {
|
||||
backgroundColor = colors[variant] || convert(sourceColors[variant]).rgb
|
||||
} else {
|
||||
backgroundColor = colors.bg || convert(sourceColors.bg)
|
||||
}
|
||||
} else {
|
||||
// background color case
|
||||
outputColor = colorFunc(
|
||||
mod,
|
||||
...deps.map((dep) => ({ ...colors[dep] }))
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!outputColor) {
|
||||
throw new Error('Couldn\'t generate color for ' + key)
|
||||
}
|
||||
|
||||
const opacitySlot = value.opacity || getOpacitySlot(key)
|
||||
const ownOpacitySlot = value.opacity
|
||||
const isLightOnDark = relativeLuminance(backgroundColor) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
|
||||
if (ownOpacitySlot === null) {
|
||||
outputColor.a = 1
|
||||
} else if (sourceColor === 'transparent') {
|
||||
outputColor.a = 0
|
||||
} else {
|
||||
const opacityOverriden = ownOpacitySlot && sourceOpacity[opacitySlot] !== undefined
|
||||
let outputColor = null
|
||||
if (sourceColor) {
|
||||
// Color is defined in source color
|
||||
let targetColor = sourceColor
|
||||
if (targetColor === 'transparent') {
|
||||
// We take only layers below current one
|
||||
const layers = getLayers(
|
||||
getLayerSlot(key),
|
||||
key,
|
||||
getOpacitySlot(key) || key,
|
||||
colors,
|
||||
opacity,
|
||||
).slice(0, -1)
|
||||
targetColor = {
|
||||
...alphaBlendLayers(convert('#FF00FF').rgb, layers),
|
||||
a: 0,
|
||||
}
|
||||
} else if (
|
||||
typeof sourceColor === 'string' &&
|
||||
sourceColor.startsWith('--')
|
||||
) {
|
||||
targetColor = computeDynamicColor(
|
||||
sourceColor,
|
||||
(variableSlot) =>
|
||||
colors[variableSlot] || sourceColors[variableSlot],
|
||||
mod,
|
||||
)
|
||||
} else if (
|
||||
typeof sourceColor === 'string' &&
|
||||
sourceColor.startsWith('#')
|
||||
) {
|
||||
targetColor = convert(targetColor).rgb
|
||||
}
|
||||
outputColor = { ...targetColor }
|
||||
} else if (value.default) {
|
||||
// same as above except in object form
|
||||
outputColor = convert(value.default).rgb
|
||||
} else {
|
||||
// calculate color
|
||||
const defaultColorFunc = (mod, dep) => ({ ...dep })
|
||||
const colorFunc = value.color || defaultColorFunc
|
||||
|
||||
const dependencySlot = deps[0]
|
||||
const dependencyColor = dependencySlot && colors[dependencySlot]
|
||||
if (value.textColor) {
|
||||
if (value.textColor === 'bw') {
|
||||
outputColor = contrastRatio(backgroundColor).rgb
|
||||
} else {
|
||||
let color = { ...colors[deps[0]] }
|
||||
if (value.color) {
|
||||
color = colorFunc(mod, ...deps.map((dep) => ({ ...colors[dep] })))
|
||||
}
|
||||
outputColor = getTextColor(
|
||||
backgroundColor,
|
||||
{ ...color },
|
||||
value.textColor === 'preserve',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// background color case
|
||||
outputColor = colorFunc(
|
||||
mod,
|
||||
...deps.map((dep) => ({ ...colors[dep] })),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!outputColor) {
|
||||
throw new Error("Couldn't generate color for " + key)
|
||||
}
|
||||
|
||||
if (!ownOpacitySlot && dependencyColor && !value.textColor && ownOpacitySlot !== null) {
|
||||
// Inheriting color from dependency (weird, i know)
|
||||
// except if it's a text color or opacity slot is set to 'null'
|
||||
outputColor.a = dependencyColor.a
|
||||
} else if (!dependencyColor && !opacitySlot) {
|
||||
// Remove any alpha channel if no dependency and no opacitySlot found
|
||||
delete outputColor.a
|
||||
} else {
|
||||
// Otherwise try to assign opacity
|
||||
if (dependencyColor && dependencyColor.a === 0) {
|
||||
// transparent dependency shall make dependents transparent too
|
||||
const opacitySlot = value.opacity || getOpacitySlot(key)
|
||||
const ownOpacitySlot = value.opacity
|
||||
|
||||
if (ownOpacitySlot === null) {
|
||||
outputColor.a = 1
|
||||
} else if (sourceColor === 'transparent') {
|
||||
outputColor.a = 0
|
||||
} else {
|
||||
// Otherwise check if opacity is overriden and use that or default value instead
|
||||
outputColor.a = Number(
|
||||
opacityOverriden
|
||||
? sourceOpacity[opacitySlot]
|
||||
: (OPACITIES[opacitySlot] || {}).defaultValue
|
||||
)
|
||||
const opacityOverriden =
|
||||
ownOpacitySlot && sourceOpacity[opacitySlot] !== undefined
|
||||
|
||||
const dependencySlot = deps[0]
|
||||
const dependencyColor = dependencySlot && colors[dependencySlot]
|
||||
|
||||
if (
|
||||
!ownOpacitySlot &&
|
||||
dependencyColor &&
|
||||
!value.textColor &&
|
||||
ownOpacitySlot !== null
|
||||
) {
|
||||
// Inheriting color from dependency (weird, i know)
|
||||
// except if it's a text color or opacity slot is set to 'null'
|
||||
outputColor.a = dependencyColor.a
|
||||
} else if (!dependencyColor && !opacitySlot) {
|
||||
// Remove any alpha channel if no dependency and no opacitySlot found
|
||||
delete outputColor.a
|
||||
} else {
|
||||
// Otherwise try to assign opacity
|
||||
if (dependencyColor && dependencyColor.a === 0) {
|
||||
// transparent dependency shall make dependents transparent too
|
||||
outputColor.a = 0
|
||||
} else {
|
||||
// Otherwise check if opacity is overriden and use that or default value instead
|
||||
outputColor.a = Number(
|
||||
opacityOverriden
|
||||
? sourceOpacity[opacitySlot]
|
||||
: (OPACITIES[opacitySlot] || {}).defaultValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isNaN(outputColor.a) || outputColor.a === undefined) {
|
||||
outputColor.a = 1
|
||||
}
|
||||
if (Number.isNaN(outputColor.a) || outputColor.a === undefined) {
|
||||
outputColor.a = 1
|
||||
}
|
||||
|
||||
if (opacitySlot) {
|
||||
return {
|
||||
colors: { ...colors, [key]: outputColor },
|
||||
opacity: { ...opacity, [opacitySlot]: outputColor.a }
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
colors: { ...colors, [key]: outputColor },
|
||||
opacity
|
||||
}
|
||||
}
|
||||
}, { colors: {}, opacity: {} })
|
||||
if (opacitySlot) {
|
||||
return {
|
||||
colors: { ...colors, [key]: outputColor },
|
||||
opacity: { ...opacity, [opacitySlot]: outputColor.a },
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
colors: { ...colors, [key]: outputColor },
|
||||
opacity,
|
||||
}
|
||||
}
|
||||
},
|
||||
{ colors: {}, opacity: {} },
|
||||
)
|
||||
|
||||
export const composePreset = (colors, radii, shadows, fonts) => {
|
||||
return {
|
||||
|
|
@ -413,14 +446,14 @@ export const composePreset = (colors, radii, shadows, fonts) => {
|
|||
...shadows.rules,
|
||||
...colors.rules,
|
||||
...radii.rules,
|
||||
...fonts.rules
|
||||
...fonts.rules,
|
||||
},
|
||||
theme: {
|
||||
...shadows.theme,
|
||||
...colors.theme,
|
||||
...radii.theme,
|
||||
...fonts.theme
|
||||
}
|
||||
...fonts.theme,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -430,7 +463,7 @@ export const generatePreset = (input) => {
|
|||
colors,
|
||||
generateRadii(input),
|
||||
generateShadows(input, colors.theme.colors, colors.mod),
|
||||
generateFonts(input)
|
||||
generateFonts(input),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -440,16 +473,17 @@ export const getCssShadow = (input, usesDropShadow) => {
|
|||
}
|
||||
|
||||
return input
|
||||
.filter(_ => usesDropShadow ? _.inset : _)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
shad.blur,
|
||||
shad.spread
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColor(shad.color, shad.alpha),
|
||||
shad.inset ? 'inset' : ''
|
||||
]).join(' ')).join(', ')
|
||||
.filter((_) => (usesDropShadow ? _.inset : _))
|
||||
.map((shad) =>
|
||||
[shad.x, shad.y, shad.blur, shad.spread]
|
||||
.map((_) => _ + 'px')
|
||||
.concat([
|
||||
getCssColor(shad.color, shad.alpha),
|
||||
shad.inset ? 'inset' : '',
|
||||
])
|
||||
.join(' '),
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
export const getCssShadowFilter = (input) => {
|
||||
|
|
@ -457,19 +491,24 @@ export const getCssShadowFilter = (input) => {
|
|||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
// drop-shadow doesn't support inset or spread
|
||||
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
// drop-shadow's blur is twice as strong compared to box-shadow
|
||||
shad.blur / 2
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColor(shad.color, shad.alpha)
|
||||
]).join(' '))
|
||||
.map(_ => `drop-shadow(${_})`)
|
||||
.join(' ')
|
||||
return (
|
||||
input
|
||||
// drop-shadow doesn't support inset or spread
|
||||
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
||||
.map((shad) =>
|
||||
[
|
||||
shad.x,
|
||||
shad.y,
|
||||
// drop-shadow's blur is twice as strong compared to box-shadow
|
||||
shad.blur / 2,
|
||||
]
|
||||
.map((_) => _ + 'px')
|
||||
.concat([getCssColor(shad.color, shad.alpha)])
|
||||
.join(' '),
|
||||
)
|
||||
.map((_) => `drop-shadow(${_})`)
|
||||
.join(' ')
|
||||
)
|
||||
}
|
||||
|
||||
export const generateColors = (themeData) => {
|
||||
|
|
@ -479,24 +518,26 @@ export const generateColors = (themeData) => {
|
|||
|
||||
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
|
||||
|
||||
const htmlColors = Object.entries(colors)
|
||||
.reduce((acc, [k, v]) => {
|
||||
const htmlColors = Object.entries(colors).reduce(
|
||||
(acc, [k, v]) => {
|
||||
if (!v) return acc
|
||||
acc.solid[k] = rgb2hex(v)
|
||||
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
|
||||
return acc
|
||||
}, { complete: {}, solid: {} })
|
||||
},
|
||||
{ complete: {}, solid: {} },
|
||||
)
|
||||
return {
|
||||
rules: {
|
||||
colors: Object.entries(htmlColors.complete)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `--${k}: ${v}`)
|
||||
.join(';')
|
||||
.join(';'),
|
||||
},
|
||||
theme: {
|
||||
colors: htmlColors.solid,
|
||||
opacity
|
||||
}
|
||||
opacity,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -504,68 +545,85 @@ export const generateRadii = (input) => {
|
|||
let inputRadii = input.radii || {}
|
||||
// v1 -> v2
|
||||
if (typeof input.btnRadius !== 'undefined') {
|
||||
inputRadii = Object
|
||||
.entries(input)
|
||||
inputRadii = Object.entries(input)
|
||||
.filter(([k]) => k.endsWith('Radius'))
|
||||
.reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {})
|
||||
.reduce((acc, e) => {
|
||||
acc[e[0].split('Radius')[0]] = e[1]
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
const radii = Object.entries(inputRadii).filter(([, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, {
|
||||
btn: 4,
|
||||
input: 4,
|
||||
checkbox: 2,
|
||||
panel: 10,
|
||||
avatar: 5,
|
||||
avatarAlt: 50,
|
||||
tooltip: 2,
|
||||
attachment: 5,
|
||||
chatMessage: inputRadii.panel
|
||||
})
|
||||
const radii = Object.entries(inputRadii)
|
||||
.filter(([, v]) => v)
|
||||
.reduce(
|
||||
(acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
},
|
||||
{
|
||||
btn: 4,
|
||||
input: 4,
|
||||
checkbox: 2,
|
||||
panel: 10,
|
||||
avatar: 5,
|
||||
avatarAlt: 50,
|
||||
tooltip: 2,
|
||||
attachment: 5,
|
||||
chatMessage: inputRadii.panel,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
rules: {
|
||||
radii: Object.entries(radii).filter(([, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
|
||||
radii: Object.entries(radii)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `--${k}Radius: ${v}px`)
|
||||
.join(';'),
|
||||
},
|
||||
theme: {
|
||||
radii
|
||||
}
|
||||
radii,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const generateFonts = (input) => {
|
||||
const fonts = Object.entries(input.fonts || {}).filter(([, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = Object.entries(v).filter(([, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, acc[k])
|
||||
return acc
|
||||
}, {
|
||||
interface: {
|
||||
family: 'sans-serif'
|
||||
},
|
||||
input: {
|
||||
family: 'inherit'
|
||||
},
|
||||
post: {
|
||||
family: 'inherit'
|
||||
},
|
||||
postCode: {
|
||||
family: 'monospace'
|
||||
}
|
||||
})
|
||||
const fonts = Object.entries(input.fonts || {})
|
||||
.filter(([, v]) => v)
|
||||
.reduce(
|
||||
(acc, [k, v]) => {
|
||||
acc[k] = Object.entries(v)
|
||||
.filter(([, v]) => v)
|
||||
.reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, acc[k])
|
||||
return acc
|
||||
},
|
||||
{
|
||||
interface: {
|
||||
family: 'sans-serif',
|
||||
},
|
||||
input: {
|
||||
family: 'inherit',
|
||||
},
|
||||
post: {
|
||||
family: 'inherit',
|
||||
},
|
||||
postCode: {
|
||||
family: 'monospace',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
rules: {
|
||||
fonts: Object
|
||||
.entries(fonts)
|
||||
fonts: Object.entries(fonts)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
|
||||
.map(([k, v]) => `--${k}Font: ${v.family}`)
|
||||
.join(';'),
|
||||
},
|
||||
theme: {
|
||||
fonts
|
||||
}
|
||||
fonts,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -576,7 +634,7 @@ const border = (top, shadow) => ({
|
|||
spread: 0,
|
||||
color: shadow ? '#000000' : '#FFFFFF',
|
||||
alpha: 0.2,
|
||||
inset: true
|
||||
inset: true,
|
||||
})
|
||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||
|
|
@ -586,63 +644,77 @@ const hoverGlow = {
|
|||
blur: 4,
|
||||
spread: 0,
|
||||
color: '--faint',
|
||||
alpha: 1
|
||||
alpha: 1,
|
||||
}
|
||||
|
||||
export const DEFAULT_SHADOWS = {
|
||||
panel: [{
|
||||
x: 1,
|
||||
y: 1,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
topBar: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
popup: [{
|
||||
x: 2,
|
||||
y: 2,
|
||||
blur: 3,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.5
|
||||
}],
|
||||
avatar: [{
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 8,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.7
|
||||
}],
|
||||
panel: [
|
||||
{
|
||||
x: 1,
|
||||
y: 1,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6,
|
||||
},
|
||||
],
|
||||
topBar: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6,
|
||||
},
|
||||
],
|
||||
popup: [
|
||||
{
|
||||
x: 2,
|
||||
y: 2,
|
||||
blur: 3,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.5,
|
||||
},
|
||||
],
|
||||
avatar: [
|
||||
{
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 8,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.7,
|
||||
},
|
||||
],
|
||||
avatarStatus: [],
|
||||
panelHeader: [],
|
||||
button: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}, ...buttonInsetFakeBorders],
|
||||
button: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1,
|
||||
},
|
||||
...buttonInsetFakeBorders,
|
||||
],
|
||||
buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
|
||||
buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
|
||||
input: [...inputInsetFakeBorders, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
inset: true,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}]
|
||||
input: [
|
||||
...inputInsetFakeBorders,
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
inset: true,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
export const generateShadows = (input, colors) => {
|
||||
// TODO this is a small hack for `mod` to work with shadows
|
||||
|
|
@ -654,58 +726,65 @@ export const generateShadows = (input, colors) => {
|
|||
popup: 'popover',
|
||||
avatar: 'bg',
|
||||
panelHeader: 'panel',
|
||||
input: 'input'
|
||||
input: 'input',
|
||||
}
|
||||
|
||||
const cleanInputShadows = Object.fromEntries(
|
||||
Object.entries(input.shadows || {})
|
||||
.map(([name, shadowSlot]) => [
|
||||
name,
|
||||
// defaulting color to black to avoid potential problems
|
||||
shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef }))
|
||||
])
|
||||
Object.entries(input.shadows || {}).map(([name, shadowSlot]) => [
|
||||
name,
|
||||
// defaulting color to black to avoid potential problems
|
||||
shadowSlot.map((shadowDef) => ({ color: '#000000', ...shadowDef })),
|
||||
]),
|
||||
)
|
||||
const inputShadows = cleanInputShadows && !input.themeEngineVersion
|
||||
? shadows2to3(cleanInputShadows, input.opacity)
|
||||
: cleanInputShadows || {}
|
||||
const inputShadows =
|
||||
cleanInputShadows && !input.themeEngineVersion
|
||||
? shadows2to3(cleanInputShadows, input.opacity)
|
||||
: cleanInputShadows || {}
|
||||
const shadows = Object.entries({
|
||||
...DEFAULT_SHADOWS,
|
||||
...inputShadows
|
||||
...inputShadows,
|
||||
}).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const slotFirstWord = slotName.replace(/[A-Z].*$/, '')
|
||||
const colorSlotName = hackContextDict[slotFirstWord]
|
||||
const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
|
||||
const isLightOnDark =
|
||||
relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
color: rgb2hex(computeDynamicColor(
|
||||
def.color,
|
||||
(variableSlot) => convert(colors[variableSlot]).rgb,
|
||||
mod
|
||||
))
|
||||
}
|
||||
], [])
|
||||
const newShadow = shadowDefs.reduce(
|
||||
(shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
color: rgb2hex(
|
||||
computeDynamicColor(
|
||||
def.color,
|
||||
(variableSlot) => convert(colors[variableSlot]).rgb,
|
||||
mod,
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
shadows: Object
|
||||
.entries(shadows)
|
||||
// TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
||||
.map(([k, v]) => [
|
||||
`--${k}Shadow: ${getCssShadow(v)}`,
|
||||
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
|
||||
`--${k}ShadowInset: ${getCssShadow(v, true)}`
|
||||
].join(';'))
|
||||
.join(';')
|
||||
shadows: Object.entries(shadows)
|
||||
// TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
||||
.map(([k, v]) =>
|
||||
[
|
||||
`--${k}Shadow: ${getCssShadow(v)}`,
|
||||
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
|
||||
`--${k}ShadowInset: ${getCssShadow(v, true)}`,
|
||||
].join(';'),
|
||||
)
|
||||
.join(';'),
|
||||
},
|
||||
theme: {
|
||||
shadows
|
||||
}
|
||||
shadows,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -715,18 +794,25 @@ export const generateShadows = (input, colors) => {
|
|||
* Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
|
||||
*/
|
||||
export const shadows2to3 = (shadows, opacity) => {
|
||||
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const isDynamic = ({ color = '#000000' }) => color.startsWith('--')
|
||||
const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
|
||||
}
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
return Object.entries(shadows).reduce(
|
||||
(shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const isDynamic = ({ color = '#000000' }) => color.startsWith('--')
|
||||
const getOpacity = ({ color }) =>
|
||||
opacity[getOpacitySlot(color.substring(2).split(',')[0])]
|
||||
const newShadow = shadowDefs.reduce(
|
||||
(shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha,
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
},
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export const colors2to3 = (colors) => {
|
||||
|
|
@ -738,12 +824,13 @@ export const colors2to3 = (colors) => {
|
|||
case 'btnText':
|
||||
return {
|
||||
...acc,
|
||||
...btnPositions
|
||||
.reduce(
|
||||
(statePositionAcc, position) =>
|
||||
({ ...statePositionAcc, ['btn' + position + 'Text']: color })
|
||||
, {}
|
||||
)
|
||||
...btnPositions.reduce(
|
||||
(statePositionAcc, position) => ({
|
||||
...statePositionAcc,
|
||||
['btn' + position + 'Text']: color,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}
|
||||
default:
|
||||
return { ...acc, [slotName]: color }
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
getTextColor,
|
||||
rgba2css,
|
||||
mixrgb,
|
||||
relativeLuminance
|
||||
relativeLuminance,
|
||||
} from '../color_convert/color_convert.js'
|
||||
|
||||
import {
|
||||
colorFunctions,
|
||||
shadowFunctions,
|
||||
process
|
||||
process,
|
||||
} from './theme3_slot_functions.js'
|
||||
|
||||
import {
|
||||
|
|
@ -20,7 +20,7 @@ import {
|
|||
getAllPossibleCombinations,
|
||||
genericRuleToSelector,
|
||||
normalizeCombination,
|
||||
findRules
|
||||
findRules,
|
||||
} from './iss_utils.js'
|
||||
import { deserializeShadow } from './iss_deserializer.js'
|
||||
|
||||
|
|
@ -36,15 +36,20 @@ const components = {
|
|||
Panel: null,
|
||||
Chat: null,
|
||||
ChatMessage: null,
|
||||
Button: null
|
||||
Button: null,
|
||||
}
|
||||
|
||||
export const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
||||
return (shadows || []).map(shadow => {
|
||||
return (shadows || []).map((shadow) => {
|
||||
let targetShadow
|
||||
if (typeof shadow === 'string') {
|
||||
if (shadow.startsWith('$')) {
|
||||
targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
|
||||
targetShadow = process(
|
||||
shadow,
|
||||
shadowFunctions,
|
||||
{ findColor, findShadow },
|
||||
{ dynamicVars, staticVars },
|
||||
)
|
||||
} else if (shadow.startsWith('--')) {
|
||||
// modifiers are completely unsupported here
|
||||
const variableSlot = shadow.substring(2)
|
||||
|
|
@ -56,21 +61,27 @@ export const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
|||
targetShadow = shadow
|
||||
}
|
||||
|
||||
const shadowArray = Array.isArray(targetShadow) ? targetShadow : [targetShadow]
|
||||
return shadowArray.map(s => ({
|
||||
const shadowArray = Array.isArray(targetShadow)
|
||||
? targetShadow
|
||||
: [targetShadow]
|
||||
return shadowArray.map((s) => ({
|
||||
...s,
|
||||
color: findColor(s.color, { dynamicVars, staticVars })
|
||||
color: findColor(s.color, { dynamicVars, staticVars }),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
export const findColor = (color, { dynamicVars, staticVars }) => {
|
||||
try {
|
||||
if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
|
||||
if (
|
||||
typeof color !== 'string' ||
|
||||
(!color.startsWith('--') && !color.startsWith('$'))
|
||||
)
|
||||
return color
|
||||
let targetColor = null
|
||||
if (color.startsWith('--')) {
|
||||
// Modifier support is pretty much for v2 themes only
|
||||
const [variable, modifier] = color.split(/,/g).map(str => str.trim())
|
||||
const [variable, modifier] = color.split(/,/g).map((str) => str.trim())
|
||||
const variableSlot = variable.substring(2)
|
||||
if (variableSlot === 'stack') {
|
||||
const { r, g, b } = dynamicVars.stacked
|
||||
|
|
@ -81,7 +92,9 @@ export const findColor = (color, { dynamicVars, staticVars }) => {
|
|||
targetColor = { r, g, b }
|
||||
} else {
|
||||
const virtualSlot = variableSlot.replace(/^parent/, '')
|
||||
targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb
|
||||
targetColor = convert(
|
||||
dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot],
|
||||
).rgb
|
||||
}
|
||||
} else {
|
||||
const staticVar = staticVars[variableSlot]
|
||||
|
|
@ -98,18 +111,32 @@ ${JSON.stringify(dynamicVars, null, 2)}`)
|
|||
}
|
||||
|
||||
if (modifier) {
|
||||
const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor
|
||||
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
||||
const effectiveBackground =
|
||||
dynamicVars.lowerLevelBackground ?? targetColor
|
||||
const isLightOnDark =
|
||||
relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
|
||||
targetColor = brightness(
|
||||
Number.parseFloat(modifier) * mod,
|
||||
targetColor,
|
||||
).rgb
|
||||
}
|
||||
}
|
||||
|
||||
if (color.startsWith('$')) {
|
||||
try {
|
||||
targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars })
|
||||
targetColor = process(
|
||||
color,
|
||||
colorFunctions,
|
||||
{ findColor },
|
||||
{ dynamicVars, staticVars },
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('Failure executing color function', e ,'\n Function: ' + color)
|
||||
console.error(
|
||||
'Failure executing color function',
|
||||
e,
|
||||
'\n Function: ' + color,
|
||||
)
|
||||
targetColor = '#FF00FF'
|
||||
}
|
||||
}
|
||||
|
|
@ -124,10 +151,17 @@ ${JSON.stringify(dynamicVars, null, 2)}\nError: ${e}`)
|
|||
}
|
||||
}
|
||||
|
||||
const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => {
|
||||
const getTextColorAlpha = (
|
||||
directives,
|
||||
intendedTextColor,
|
||||
dynamicVars,
|
||||
staticVars,
|
||||
) => {
|
||||
const opacity = directives.textOpacity
|
||||
const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb
|
||||
const textColor = convert(findColor(intendedTextColor, { dynamicVars, staticVars })).rgb
|
||||
const textColor = convert(
|
||||
findColor(intendedTextColor, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
if (opacity === null || opacity === undefined || opacity >= 1) {
|
||||
return convert(textColor).hex
|
||||
}
|
||||
|
|
@ -148,26 +182,29 @@ const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVar
|
|||
// Loading all style.js[on] files dynamically
|
||||
const componentsContext = import.meta.glob(
|
||||
['/src/**/*.style.js', '/src/**/*.style.json'],
|
||||
{ eager: true }
|
||||
{ eager: true },
|
||||
)
|
||||
Object.keys(componentsContext).forEach(key => {
|
||||
Object.keys(componentsContext).forEach((key) => {
|
||||
const component = componentsContext[key].default
|
||||
if (components[component.name] != null) {
|
||||
console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
|
||||
console.warn(
|
||||
`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`,
|
||||
)
|
||||
}
|
||||
components[component.name] = component
|
||||
})
|
||||
|
||||
Object.keys(components).forEach(key => {
|
||||
Object.keys(components).forEach((key) => {
|
||||
if (key === 'Root') return
|
||||
components.Root.validInnerComponents = components.Root.validInnerComponents || []
|
||||
components.Root.validInnerComponents =
|
||||
components.Root.validInnerComponents || []
|
||||
components.Root.validInnerComponents.push(key)
|
||||
})
|
||||
|
||||
Object.keys(components).forEach(key => {
|
||||
Object.keys(components).forEach((key) => {
|
||||
const component = components[key]
|
||||
const { validInnerComponents = [] } = component
|
||||
validInnerComponents.forEach(inner => {
|
||||
validInnerComponents.forEach((inner) => {
|
||||
const child = components[inner]
|
||||
component.possibleChildren = component.possibleChildren || []
|
||||
component.possibleChildren.push(child)
|
||||
|
|
@ -176,7 +213,6 @@ Object.keys(components).forEach(key => {
|
|||
})
|
||||
})
|
||||
|
||||
|
||||
const engineChecksum = sum(components)
|
||||
|
||||
const ruleToSelector = genericRuleToSelector(components)
|
||||
|
|
@ -206,7 +242,7 @@ export const init = ({
|
|||
liteMode = false,
|
||||
editMode = false,
|
||||
onlyNormalState = false,
|
||||
initialStaticVars = {}
|
||||
initialStaticVars = {},
|
||||
}) => {
|
||||
const rootComponentName = 'Root'
|
||||
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
|
||||
|
|
@ -216,10 +252,16 @@ export const init = ({
|
|||
|
||||
const rulesetUnsorted = [
|
||||
...Object.values(components)
|
||||
.map(c => (c.defaultRules || []).map(r => ({ source: 'Built-in', component: c.name, ...r })))
|
||||
.map((c) =>
|
||||
(c.defaultRules || []).map((r) => ({
|
||||
source: 'Built-in',
|
||||
component: c.name,
|
||||
...r,
|
||||
})),
|
||||
)
|
||||
.reduce((acc, arr) => [...acc, ...arr], []),
|
||||
...inputRuleset
|
||||
].map(rule => {
|
||||
...inputRuleset,
|
||||
].map((rule) => {
|
||||
normalizeCombination(rule)
|
||||
let currentParent = rule.parent
|
||||
while (currentParent) {
|
||||
|
|
@ -245,8 +287,8 @@ export const init = ({
|
|||
aScore += a.variant !== 'normal' ? 100 : 0
|
||||
bScore += b.variant !== 'normal' ? 100 : 0
|
||||
|
||||
aScore += a.state.filter(x => x !== 'normal').length * 1000
|
||||
bScore += b.state.filter(x => x !== 'normal').length * 1000
|
||||
aScore += a.state.filter((x) => x !== 'normal').length * 1000
|
||||
bScore += b.state.filter((x) => x !== 'normal').length * 1000
|
||||
|
||||
aScore += a.component === 'Text' ? 1 : 0
|
||||
bScore += b.component === 'Text' ? 1 : 0
|
||||
|
|
@ -263,24 +305,44 @@ export const init = ({
|
|||
.map(({ data }) => data)
|
||||
|
||||
if (!ultimateBackgroundColor) {
|
||||
console.warn('No ultimate background color provided, falling back to panel color')
|
||||
const rootRule = ruleset.findLast((x) => (x.component === 'Root' && x.directives?.['--bg']))
|
||||
console.warn(
|
||||
'No ultimate background color provided, falling back to panel color',
|
||||
)
|
||||
const rootRule = ruleset.findLast(
|
||||
(x) => x.component === 'Root' && x.directives?.['--bg'],
|
||||
)
|
||||
ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
|
||||
}
|
||||
|
||||
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
|
||||
const transparentComponents = new Set(Object.values(components).filter(c => c.transparent).map(c => c.name))
|
||||
const nonEditableComponents = new Set(Object.values(components).filter(c => c.notEditable).map(c => c.name))
|
||||
const virtualComponents = new Set(
|
||||
Object.values(components)
|
||||
.filter((c) => c.virtual)
|
||||
.map((c) => c.name),
|
||||
)
|
||||
const transparentComponents = new Set(
|
||||
Object.values(components)
|
||||
.filter((c) => c.transparent)
|
||||
.map((c) => c.name),
|
||||
)
|
||||
const nonEditableComponents = new Set(
|
||||
Object.values(components)
|
||||
.filter((c) => c.notEditable)
|
||||
.map((c) => c.name),
|
||||
)
|
||||
const extraCompileComponents = new Set([])
|
||||
|
||||
Object.values(components).forEach(component => {
|
||||
const relevantRules = ruleset.filter(r => r.component === component.name)
|
||||
const backgrounds = relevantRules.map(r => r.directives.background).filter(x => x)
|
||||
const opacities = relevantRules.map(r => r.directives.opacity).filter(x => x)
|
||||
Object.values(components).forEach((component) => {
|
||||
const relevantRules = ruleset.filter((r) => r.component === component.name)
|
||||
const backgrounds = relevantRules
|
||||
.map((r) => r.directives.background)
|
||||
.filter((x) => x)
|
||||
const opacities = relevantRules
|
||||
.map((r) => r.directives.opacity)
|
||||
.filter((x) => x)
|
||||
if (
|
||||
backgrounds.some(x => x.match(/--parent/)) ||
|
||||
opacities.some(x => x != null && x < 1))
|
||||
{
|
||||
backgrounds.some((x) => x.match(/--parent/)) ||
|
||||
opacities.some((x) => x != null && x < 1)
|
||||
) {
|
||||
extraCompileComponents.add(component.name)
|
||||
}
|
||||
})
|
||||
|
|
@ -299,25 +361,26 @@ export const init = ({
|
|||
// FIXME hack for editor until it supports handling component backgrounds
|
||||
lowerLevelBackground = '#00FFFF'
|
||||
}
|
||||
const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
|
||||
const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
|
||||
const lowerLevelVirtualDirectives =
|
||||
computed[lowerLevelSelector]?.virtualDirectives
|
||||
const lowerLevelVirtualDirectivesRaw =
|
||||
computed[lowerLevelSelector]?.virtualDirectivesRaw
|
||||
|
||||
const dynamicVars = computed[selector] || {
|
||||
lowerLevelSelector,
|
||||
lowerLevelBackground,
|
||||
lowerLevelVirtualDirectives,
|
||||
lowerLevelVirtualDirectivesRaw
|
||||
lowerLevelVirtualDirectivesRaw,
|
||||
}
|
||||
|
||||
// Inheriting all of the applicable rules
|
||||
const existingRules = ruleset.filter(findRules(combination))
|
||||
const computedDirectives =
|
||||
existingRules
|
||||
.map(r => r.directives)
|
||||
.reduce((acc, directives) => ({ ...acc, ...directives }), {})
|
||||
const computedDirectives = existingRules
|
||||
.map((r) => r.directives)
|
||||
.reduce((acc, directives) => ({ ...acc, ...directives }), {})
|
||||
const computedRule = {
|
||||
...combination,
|
||||
directives: computedDirectives
|
||||
directives: computedDirectives,
|
||||
}
|
||||
|
||||
computed[selector] = computed[selector] || {}
|
||||
|
|
@ -327,7 +390,8 @@ export const init = ({
|
|||
// avoid putting more stuff into actual CSS
|
||||
computed[selector].virtualDirectives = {}
|
||||
// but still be able to access it i.e. from --parent
|
||||
computed[selector].virtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw || {}
|
||||
computed[selector].virtualDirectivesRaw =
|
||||
computed[lowerLevelSelector]?.virtualDirectivesRaw || {}
|
||||
|
||||
if (virtualComponents.has(combination.component)) {
|
||||
const virtualName = [
|
||||
|
|
@ -335,22 +399,37 @@ export const init = ({
|
|||
combination.component.toLowerCase(),
|
||||
combination.variant === 'normal'
|
||||
? ''
|
||||
: combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(),
|
||||
...sortBy(combination.state.filter(x => x !== 'normal')).map(state => state[0].toUpperCase() + state.slice(1).toLowerCase())
|
||||
: combination.variant[0].toUpperCase() +
|
||||
combination.variant.slice(1).toLowerCase(),
|
||||
...sortBy(combination.state.filter((x) => x !== 'normal')).map(
|
||||
(state) => state[0].toUpperCase() + state.slice(1).toLowerCase(),
|
||||
),
|
||||
].join('')
|
||||
|
||||
let inheritedTextColor = computedDirectives.textColor
|
||||
let inheritedTextAuto = computedDirectives.textAuto
|
||||
let inheritedTextOpacity = computedDirectives.textOpacity
|
||||
let inheritedTextOpacityMode = computedDirectives.textOpacityMode
|
||||
const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ')
|
||||
const lowerLevelTextSelector = [
|
||||
...selector.split(/ /g).slice(0, -1),
|
||||
soloSelector,
|
||||
].join(' ')
|
||||
const lowerLevelTextRule = computed[lowerLevelTextSelector]
|
||||
|
||||
if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
|
||||
inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
|
||||
inheritedTextAuto = computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
|
||||
inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
|
||||
inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
|
||||
if (
|
||||
inheritedTextColor == null ||
|
||||
inheritedTextOpacity == null ||
|
||||
inheritedTextOpacityMode == null
|
||||
) {
|
||||
inheritedTextColor =
|
||||
computedDirectives.textColor ?? lowerLevelTextRule.textColor
|
||||
inheritedTextAuto =
|
||||
computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
|
||||
inheritedTextOpacity =
|
||||
computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
|
||||
inheritedTextOpacityMode =
|
||||
computedDirectives.textOpacityMode ??
|
||||
lowerLevelTextRule.textOpacityMode
|
||||
}
|
||||
|
||||
const newTextRule = {
|
||||
|
|
@ -360,26 +439,37 @@ export const init = ({
|
|||
textColor: inheritedTextColor,
|
||||
textAuto: inheritedTextAuto ?? 'preserve',
|
||||
textOpacity: inheritedTextOpacity,
|
||||
textOpacityMode: inheritedTextOpacityMode
|
||||
}
|
||||
textOpacityMode: inheritedTextOpacityMode,
|
||||
},
|
||||
}
|
||||
|
||||
dynamicVars.inheritedBackground = lowerLevelBackground
|
||||
dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb
|
||||
|
||||
const intendedTextColor = convert(findColor(inheritedTextColor, { dynamicVars, staticVars })).rgb
|
||||
const textColor = newTextRule.directives.textAuto === 'no-auto'
|
||||
? intendedTextColor
|
||||
: getTextColor(
|
||||
convert(stacked[lowerLevelSelector]).rgb,
|
||||
intendedTextColor,
|
||||
newTextRule.directives.textAuto === 'preserve'
|
||||
)
|
||||
const virtualDirectives = { ...(computed[lowerLevelSelector].virtualDirectives || {}) }
|
||||
const virtualDirectivesRaw = { ...(computed[lowerLevelSelector].virtualDirectivesRaw || {}) }
|
||||
const intendedTextColor = convert(
|
||||
findColor(inheritedTextColor, { dynamicVars, staticVars }),
|
||||
).rgb
|
||||
const textColor =
|
||||
newTextRule.directives.textAuto === 'no-auto'
|
||||
? intendedTextColor
|
||||
: getTextColor(
|
||||
convert(stacked[lowerLevelSelector]).rgb,
|
||||
intendedTextColor,
|
||||
newTextRule.directives.textAuto === 'preserve',
|
||||
)
|
||||
const virtualDirectives = {
|
||||
...(computed[lowerLevelSelector].virtualDirectives || {}),
|
||||
}
|
||||
const virtualDirectivesRaw = {
|
||||
...(computed[lowerLevelSelector].virtualDirectivesRaw || {}),
|
||||
}
|
||||
|
||||
// Storing color data in lower layer to use as custom css properties
|
||||
virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars)
|
||||
virtualDirectives[virtualName] = getTextColorAlpha(
|
||||
newTextRule.directives,
|
||||
textColor,
|
||||
dynamicVars,
|
||||
)
|
||||
virtualDirectivesRaw[virtualName] = textColor
|
||||
|
||||
computed[lowerLevelSelector].virtualDirectives = virtualDirectives
|
||||
|
|
@ -391,13 +481,14 @@ export const init = ({
|
|||
...combination,
|
||||
directives: {},
|
||||
virtualDirectives,
|
||||
virtualDirectivesRaw
|
||||
virtualDirectivesRaw,
|
||||
}
|
||||
} else {
|
||||
computed[selector] = computed[selector] || {}
|
||||
|
||||
// TODO: DEFAULT TEXT COLOR
|
||||
const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
|
||||
const lowerLevelStackedBackground =
|
||||
stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
|
||||
|
||||
if (computedDirectives.background) {
|
||||
let inheritRule = null
|
||||
|
|
@ -405,27 +496,37 @@ export const init = ({
|
|||
findRules({
|
||||
component: combination.component,
|
||||
variant: combination.variant,
|
||||
parent: combination.parent
|
||||
})
|
||||
parent: combination.parent,
|
||||
}),
|
||||
)
|
||||
const lastVariantRule = variantRules[variantRules.length - 1]
|
||||
if (lastVariantRule) {
|
||||
inheritRule = lastVariantRule
|
||||
} else {
|
||||
const normalRules = ruleset.filter(findRules({
|
||||
component: combination.component,
|
||||
parent: combination.parent
|
||||
}))
|
||||
const normalRules = ruleset.filter(
|
||||
findRules({
|
||||
component: combination.component,
|
||||
parent: combination.parent,
|
||||
}),
|
||||
)
|
||||
const lastNormalRule = normalRules[normalRules.length - 1]
|
||||
inheritRule = lastNormalRule
|
||||
}
|
||||
|
||||
const inheritSelector = ruleToSelector({ ...inheritRule, parent: combination.parent }, true)
|
||||
const inheritSelector = ruleToSelector(
|
||||
{ ...inheritRule, parent: combination.parent },
|
||||
true,
|
||||
)
|
||||
const inheritedBackground = computed[inheritSelector].background
|
||||
|
||||
dynamicVars.inheritedBackground = inheritedBackground
|
||||
|
||||
const rgb = convert(findColor(computedDirectives.background, { dynamicVars, staticVars })).rgb
|
||||
const rgb = convert(
|
||||
findColor(computedDirectives.background, {
|
||||
dynamicVars,
|
||||
staticVars,
|
||||
}),
|
||||
).rgb
|
||||
|
||||
if (!stacked[selector]) {
|
||||
let blend
|
||||
|
|
@ -435,31 +536,48 @@ export const init = ({
|
|||
} else if (alpha <= 0) {
|
||||
blend = lowerLevelStackedBackground
|
||||
} else {
|
||||
blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground)
|
||||
blend = alphaBlend(
|
||||
rgb,
|
||||
computedDirectives.opacity,
|
||||
lowerLevelStackedBackground,
|
||||
)
|
||||
}
|
||||
stacked[selector] = blend
|
||||
computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 }
|
||||
computed[selector].background = {
|
||||
...rgb,
|
||||
a: computedDirectives.opacity ?? 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (computedDirectives.shadow) {
|
||||
dynamicVars.shadow = flattenDeep(findShadow(flattenDeep(computedDirectives.shadow), { dynamicVars, staticVars }))
|
||||
dynamicVars.shadow = flattenDeep(
|
||||
findShadow(flattenDeep(computedDirectives.shadow), {
|
||||
dynamicVars,
|
||||
staticVars,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (!stacked[selector]) {
|
||||
computedDirectives.background = 'transparent'
|
||||
computedDirectives.opacity = 0
|
||||
stacked[selector] = lowerLevelStackedBackground
|
||||
computed[selector].background = { ...lowerLevelStackedBackground, a: 0 }
|
||||
computed[selector].background = {
|
||||
...lowerLevelStackedBackground,
|
||||
a: 0,
|
||||
}
|
||||
}
|
||||
|
||||
dynamicVars.stacked = stacked[selector]
|
||||
dynamicVars.background = computed[selector].background
|
||||
|
||||
const dynamicSlots = Object.entries(computedDirectives).filter(([k]) => k.startsWith('--'))
|
||||
const dynamicSlots = Object.entries(computedDirectives).filter(([k]) =>
|
||||
k.startsWith('--'),
|
||||
)
|
||||
|
||||
dynamicSlots.forEach(([k, v]) => {
|
||||
const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme!
|
||||
const [type, value] = v.split('|').map((x) => x.trim()) // woah, Extreme!
|
||||
switch (type) {
|
||||
case 'color': {
|
||||
const color = findColor(value, { dynamicVars, staticVars })
|
||||
|
|
@ -470,7 +588,10 @@ export const init = ({
|
|||
break
|
||||
}
|
||||
case 'shadow': {
|
||||
const shadow = value.split(/,/g).map(s => s.trim()).filter(x => x)
|
||||
const shadow = value
|
||||
.split(/,/g)
|
||||
.map((s) => s.trim())
|
||||
.filter((x) => x)
|
||||
dynamicVars[k] = shadow
|
||||
if (combination.component === rootComponentName) {
|
||||
staticVars[k.substring(2)] = shadow
|
||||
|
|
@ -491,81 +612,97 @@ export const init = ({
|
|||
dynamicVars,
|
||||
selector: cssSelector,
|
||||
...combination,
|
||||
directives: computedDirectives
|
||||
directives: computedDirectives,
|
||||
}
|
||||
|
||||
return rule
|
||||
}
|
||||
} catch (e) {
|
||||
const { component, variant, state } = combination
|
||||
throw new Error(`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`)
|
||||
throw new Error(
|
||||
`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const processInnerComponent = (component, parent) => {
|
||||
const combinations = []
|
||||
const {
|
||||
states: originalStates = {},
|
||||
variants: originalVariants = {}
|
||||
} = component
|
||||
const { states: originalStates = {}, variants: originalVariants = {} } =
|
||||
component
|
||||
|
||||
let validInnerComponents
|
||||
if (editMode) {
|
||||
const temp = (component.validInnerComponentsLite || component.validInnerComponents || [])
|
||||
validInnerComponents = temp
|
||||
.filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
|
||||
const temp =
|
||||
component.validInnerComponentsLite ||
|
||||
component.validInnerComponents ||
|
||||
[]
|
||||
validInnerComponents = temp.filter(
|
||||
(c) => virtualComponents.has(c) && !nonEditableComponents.has(c),
|
||||
)
|
||||
} else if (liteMode) {
|
||||
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
|
||||
} else if (component.name === 'Root' || component.states != null || component.background?.includes('--parent')) {
|
||||
validInnerComponents =
|
||||
component.validInnerComponentsLite ||
|
||||
component.validInnerComponents ||
|
||||
[]
|
||||
} else if (
|
||||
component.name === 'Root' ||
|
||||
component.states != null ||
|
||||
component.background?.includes('--parent')
|
||||
) {
|
||||
validInnerComponents = component.validInnerComponents || []
|
||||
} else {
|
||||
validInnerComponents = component
|
||||
.validInnerComponents
|
||||
?.filter(
|
||||
c => virtualComponents.has(c)
|
||||
|| transparentComponents.has(c)
|
||||
|| extraCompileComponents.has(c)
|
||||
)
|
||||
|| []
|
||||
validInnerComponents =
|
||||
component.validInnerComponents?.filter(
|
||||
(c) =>
|
||||
virtualComponents.has(c) ||
|
||||
transparentComponents.has(c) ||
|
||||
extraCompileComponents.has(c),
|
||||
) || []
|
||||
}
|
||||
|
||||
// Normalizing states and variants to always include "normal"
|
||||
const states = { normal: '', ...originalStates }
|
||||
const variants = { normal: '', ...originalVariants }
|
||||
const innerComponents = (validInnerComponents).map(name => {
|
||||
const innerComponents = validInnerComponents.map((name) => {
|
||||
const result = components[name]
|
||||
if (result === undefined) console.error(`Component ${component.name} references a component ${name} which does not exist!`)
|
||||
if (result === undefined)
|
||||
console.error(
|
||||
`Component ${component.name} references a component ${name} which does not exist!`,
|
||||
)
|
||||
return result
|
||||
})
|
||||
|
||||
// Optimization: we only really need combinations without "normal" because all states implicitly have it
|
||||
const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal')
|
||||
const stateCombinations = (onlyNormalState && !virtualComponents.has(component.name))
|
||||
? [
|
||||
['normal']
|
||||
]
|
||||
: [
|
||||
['normal'],
|
||||
...getAllPossibleCombinations(permutationStateKeys)
|
||||
.map(combination => ['normal', ...combination])
|
||||
.filter(combo => {
|
||||
// Optimization: filter out some hard-coded combinations that don't make sense
|
||||
if (combo.indexOf('disabled') >= 0) {
|
||||
return !(
|
||||
combo.indexOf('hover') >= 0 ||
|
||||
const permutationStateKeys = Object.keys(states).filter(
|
||||
(s) => s !== 'normal',
|
||||
)
|
||||
const stateCombinations =
|
||||
onlyNormalState && !virtualComponents.has(component.name)
|
||||
? [['normal']]
|
||||
: [
|
||||
['normal'],
|
||||
...getAllPossibleCombinations(permutationStateKeys)
|
||||
.map((combination) => ['normal', ...combination])
|
||||
.filter((combo) => {
|
||||
// Optimization: filter out some hard-coded combinations that don't make sense
|
||||
if (combo.indexOf('disabled') >= 0) {
|
||||
return !(
|
||||
combo.indexOf('hover') >= 0 ||
|
||||
combo.indexOf('focused') >= 0 ||
|
||||
combo.indexOf('pressed') >= 0
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
]
|
||||
)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
]
|
||||
|
||||
const stateVariantCombination = Object.keys(variants).map(variant => {
|
||||
return stateCombinations.map(state => ({ variant, state }))
|
||||
}).reduce((acc, x) => [...acc, ...x], [])
|
||||
const stateVariantCombination = Object.keys(variants)
|
||||
.map((variant) => {
|
||||
return stateCombinations.map((state) => ({ variant, state }))
|
||||
})
|
||||
.reduce((acc, x) => [...acc, ...x], [])
|
||||
|
||||
stateVariantCombination.forEach(combination => {
|
||||
stateVariantCombination.forEach((combination) => {
|
||||
combination.component = component.name
|
||||
combination.lazy = component.lazy || parent?.lazy
|
||||
combination.parent = parent
|
||||
|
|
@ -576,16 +713,16 @@ export const init = ({
|
|||
if (
|
||||
!liteMode &&
|
||||
parent?.component !== 'Root' &&
|
||||
!virtualComponents.has(component.name) &&
|
||||
!transparentComponents.has(component.name) &&
|
||||
extraCompileComponents.has(component.name)
|
||||
!virtualComponents.has(component.name) &&
|
||||
!transparentComponents.has(component.name) &&
|
||||
extraCompileComponents.has(component.name)
|
||||
) {
|
||||
combination.lazy = true
|
||||
}
|
||||
|
||||
combinations.push(combination)
|
||||
|
||||
innerComponents.forEach(innerComponent => {
|
||||
innerComponents.forEach((innerComponent) => {
|
||||
combinations.push(...processInnerComponent(innerComponent, combination))
|
||||
})
|
||||
})
|
||||
|
|
@ -594,19 +731,23 @@ export const init = ({
|
|||
}
|
||||
|
||||
const t0 = performance.now()
|
||||
const combinations = processInnerComponent(components[rootComponentName] ?? components.Root)
|
||||
const combinations = processInnerComponent(
|
||||
components[rootComponentName] ?? components.Root,
|
||||
)
|
||||
const t1 = performance.now()
|
||||
if (debug) {
|
||||
console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
|
||||
}
|
||||
|
||||
const result = combinations.map((combination) => {
|
||||
if (combination.lazy) {
|
||||
return async () => processCombination(combination)
|
||||
} else {
|
||||
return processCombination(combination)
|
||||
}
|
||||
}).filter(x => x)
|
||||
const result = combinations
|
||||
.map((combination) => {
|
||||
if (combination.lazy) {
|
||||
return async () => processCombination(combination)
|
||||
} else {
|
||||
return processCombination(combination)
|
||||
}
|
||||
})
|
||||
.filter((x) => x)
|
||||
const t2 = performance.now()
|
||||
if (debug) {
|
||||
console.debug('Eager processing took ' + (t2 - t1) + ' ms')
|
||||
|
|
@ -616,7 +757,7 @@ export const init = ({
|
|||
const eager = []
|
||||
const lazy = []
|
||||
|
||||
result.forEach(x => {
|
||||
result.forEach((x) => {
|
||||
if (typeof x === 'function') {
|
||||
lazy.push(x)
|
||||
} else {
|
||||
|
|
@ -629,6 +770,6 @@ export const init = ({
|
|||
eager,
|
||||
staticVars,
|
||||
engineChecksum,
|
||||
themeChecksum: sum([lazy, eager])
|
||||
themeChecksum: sum([lazy, eager]),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ import apiService from '../api/api.service.js'
|
|||
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
const update = ({ store, statuses, timeline, showImmediately, userId, listId, pagination }) => {
|
||||
const update = ({
|
||||
store,
|
||||
statuses,
|
||||
timeline,
|
||||
showImmediately,
|
||||
userId,
|
||||
listId,
|
||||
pagination,
|
||||
}) => {
|
||||
const ccTimeline = camelCase(timeline)
|
||||
|
||||
store.dispatch('addNewStatuses', {
|
||||
|
|
@ -13,7 +21,7 @@ const update = ({ store, statuses, timeline, showImmediately, userId, listId, pa
|
|||
listId,
|
||||
statuses,
|
||||
showImmediately,
|
||||
pagination
|
||||
pagination,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +37,7 @@ const fetchAndUpdate = ({
|
|||
bookmarkFolderId = false,
|
||||
tag = false,
|
||||
until,
|
||||
since
|
||||
since,
|
||||
}) => {
|
||||
const args = { timeline, credentials }
|
||||
const rootState = store.rootState || store.state
|
||||
|
|
@ -54,14 +62,18 @@ const fetchAndUpdate = ({
|
|||
args.bookmarkFolderId = bookmarkFolderId
|
||||
args.tag = tag
|
||||
args.withMuted = !hideMutedPosts
|
||||
if (loggedIn && ['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)) {
|
||||
if (
|
||||
loggedIn &&
|
||||
['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)
|
||||
) {
|
||||
args.replyVisibility = replyVisibility
|
||||
}
|
||||
|
||||
const numStatusesBeforeFetch = timelineData.statuses.length
|
||||
|
||||
return apiService.fetchTimeline(args)
|
||||
.then(response => {
|
||||
return apiService
|
||||
.fetchTimeline(args)
|
||||
.then((response) => {
|
||||
if (response.errors) {
|
||||
if (timeline === 'favorites') {
|
||||
rootState.instance.pleromaPublicFavouritesAvailable = false
|
||||
|
|
@ -71,10 +83,23 @@ const fetchAndUpdate = ({
|
|||
}
|
||||
|
||||
const { data: statuses, pagination } = response
|
||||
if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
|
||||
if (
|
||||
!older &&
|
||||
statuses.length >= 20 &&
|
||||
!timelineData.loading &&
|
||||
numStatusesBeforeFetch > 0
|
||||
) {
|
||||
store.dispatch('queueFlush', { timeline, id: timelineData.maxId })
|
||||
}
|
||||
update({ store, statuses, timeline, showImmediately, userId, listId, pagination })
|
||||
update({
|
||||
store,
|
||||
statuses,
|
||||
timeline,
|
||||
showImmediately,
|
||||
userId,
|
||||
listId,
|
||||
pagination,
|
||||
})
|
||||
return { statuses, pagination }
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
@ -82,26 +107,54 @@ const fetchAndUpdate = ({
|
|||
level: 'error',
|
||||
messageKey: 'timeline.error',
|
||||
messageArgs: [error.message],
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, statusId = false, bookmarkFolderId = false, tag = false }) => {
|
||||
const startFetching = ({
|
||||
timeline = 'friends',
|
||||
credentials,
|
||||
store,
|
||||
userId = false,
|
||||
listId = false,
|
||||
statusId = false,
|
||||
bookmarkFolderId = false,
|
||||
tag = false,
|
||||
}) => {
|
||||
const rootState = store.rootState || store.state
|
||||
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
|
||||
const showImmediately = timelineData.visibleStatuses.length === 0
|
||||
timelineData.userId = userId
|
||||
timelineData.listId = listId
|
||||
timelineData.bookmarkFolderId = bookmarkFolderId
|
||||
fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, statusId, bookmarkFolderId, tag })
|
||||
fetchAndUpdate({
|
||||
timeline,
|
||||
credentials,
|
||||
store,
|
||||
showImmediately,
|
||||
userId,
|
||||
listId,
|
||||
statusId,
|
||||
bookmarkFolderId,
|
||||
tag,
|
||||
})
|
||||
const boundFetchAndUpdate = () =>
|
||||
fetchAndUpdate({ timeline, credentials, store, userId, listId, statusId, bookmarkFolderId, tag })
|
||||
fetchAndUpdate({
|
||||
timeline,
|
||||
credentials,
|
||||
store,
|
||||
userId,
|
||||
listId,
|
||||
statusId,
|
||||
bookmarkFolderId,
|
||||
tag,
|
||||
})
|
||||
return promiseInterval(boundFetchAndUpdate, 10000)
|
||||
}
|
||||
const timelineFetcher = {
|
||||
fetchAndUpdate,
|
||||
startFetching
|
||||
startFetching,
|
||||
}
|
||||
|
||||
export default timelineFetcher
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const highlightStyle = (prefs) => {
|
|||
const customProps = {
|
||||
'--____highlight-solidColor': solidColor,
|
||||
'--____highlight-tintColor': tintColor,
|
||||
'--____highlight-tintColor2': tintColor2
|
||||
'--____highlight-tintColor2': tintColor2,
|
||||
}
|
||||
if (type === 'striped') {
|
||||
return {
|
||||
|
|
@ -20,15 +20,15 @@ const highlightStyle = (prefs) => {
|
|||
`${tintColor} ,`,
|
||||
`${tintColor} 20px,`,
|
||||
`${tintColor2} 20px,`,
|
||||
`${tintColor2} 40px`
|
||||
`${tintColor2} 40px`,
|
||||
].join(' '),
|
||||
backgroundPosition: '0 0',
|
||||
...customProps
|
||||
...customProps,
|
||||
}
|
||||
} else if (type === 'solid') {
|
||||
return {
|
||||
backgroundColor: tintColor2,
|
||||
...customProps
|
||||
...customProps,
|
||||
}
|
||||
} else if (type === 'side') {
|
||||
return {
|
||||
|
|
@ -36,21 +36,18 @@ const highlightStyle = (prefs) => {
|
|||
'linear-gradient(to right,',
|
||||
`${solidColor} ,`,
|
||||
`${solidColor} 2px,`,
|
||||
'transparent 6px'
|
||||
'transparent 6px',
|
||||
].join(' '),
|
||||
backgroundPosition: '0 0',
|
||||
...customProps
|
||||
...customProps,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const highlightClass = (user) => {
|
||||
return 'USER____' + user.screen_name
|
||||
?.replace(/\./g, '_')
|
||||
.replace(/@/g, '_AT_')
|
||||
return (
|
||||
'USER____' + user.screen_name?.replace(/\./g, '_').replace(/@/g, '_AT_')
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
highlightClass,
|
||||
highlightStyle
|
||||
}
|
||||
export { highlightClass, highlightStyle }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { includes } from 'lodash'
|
||||
|
||||
const generateProfileLink = (id, screenName, restrictedNicknames) => {
|
||||
const complicated = !screenName || (isExternal(screenName) || includes(restrictedNicknames, screenName))
|
||||
const complicated =
|
||||
!screenName ||
|
||||
isExternal(screenName) ||
|
||||
includes(restrictedNicknames, screenName)
|
||||
return {
|
||||
name: (complicated ? 'external-user-profile' : 'user-profile'),
|
||||
params: (complicated ? { id } : { name: screenName })
|
||||
name: complicated ? 'external-user-profile' : 'user-profile',
|
||||
params: complicated ? { id } : { name: screenName },
|
||||
}
|
||||
}
|
||||
|
||||
const isExternal = screenName => screenName && screenName.includes('@')
|
||||
const isExternal = (screenName) => screenName && screenName.includes('@')
|
||||
|
||||
export default generateProfileLink
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue