diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/alert.style.js | 4 | ||||
| -rw-r--r-- | src/components/button.style.js | 4 | ||||
| -rw-r--r-- | src/components/input.style.js | 2 | ||||
| -rw-r--r-- | src/components/poll/poll.vue | 7 | ||||
| -rw-r--r-- | src/components/settings_modal/tabs/general_tab.vue | 23 | ||||
| -rw-r--r-- | src/components/timeago/timeago.vue | 57 | ||||
| -rw-r--r-- | src/i18n/en.json | 6 | ||||
| -rw-r--r-- | src/i18n/nan-TW.json | 43 | ||||
| -rw-r--r-- | src/modules/config.js | 4 | ||||
| -rw-r--r-- | src/modules/instance.js | 2 | ||||
| -rw-r--r-- | src/services/date_utils/date_utils.js | 43 | ||||
| -rw-r--r-- | src/services/locale/locale.service.js | 2 | ||||
| -rw-r--r-- | src/services/theme_data/iss_deserializer.js | 157 | ||||
| -rw-r--r-- | src/services/theme_data/iss_serializer.js | 48 | ||||
| -rw-r--r-- | src/services/theme_data/theme_data_3.service.js | 16 |
15 files changed, 396 insertions, 22 deletions
diff --git a/src/components/alert.style.js b/src/components/alert.style.js index 19bd4bbb..abbeb5ba 100644 --- a/src/components/alert.style.js +++ b/src/components/alert.style.js @@ -27,7 +27,9 @@ export default { component: 'Alert' }, component: 'Border', - textColor: '--parent' + directives: { + textColor: '--parent' + } }, { variant: 'error', diff --git a/src/components/button.style.js b/src/components/button.style.js index c31b0a3e..1423d5c7 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -34,8 +34,8 @@ export default { directives: { '--defaultButtonHoverGlow': 'shadow | 0 0 4 --text', '--defaultButtonShadow': 'shadow | 0 0 2 #000000', - '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2)', - '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)' + '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)', + '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)' } }, { diff --git a/src/components/input.style.js b/src/components/input.style.js index e06dc7cb..6ad6cf90 100644 --- a/src/components/input.style.js +++ b/src/components/input.style.js @@ -27,7 +27,7 @@ export default { { component: 'Root', directives: { - '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)' + '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)' } }, { diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index 580e5377..e12f3e61 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -76,6 +76,13 @@ > {{ $t('polls.vote') }} </button> + <span + v-if="poll.pleroma?.non_anonymous" + :title="$t('polls.non_anonymous_title')" + > + {{ $t('polls.non_anonymous') }} + · + </span> <div class="total"> <template v-if="typeof poll.voters_count === 'number'"> {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }} diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 4ece6cf4..82d5da89 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -217,6 +217,29 @@ {{ $t('settings.no_rich_text_description') }} </BooleanSetting> </li> + <li> + <BooleanSetting + path="useAbsoluteTimeFormat" + expert="1" + > + {{ $t('settings.absolute_time_format') }} + </BooleanSetting> + </li> + <ul + class="setting-list suboptions" + v-if="mergedConfig.useAbsoluteTimeFormat" + > + <li> + <UnitSetting + path="absoluteTimeFormatMinAge" + unit-set="time" + :units="['s', 'm', 'h', 'd']" + :min="0" + > + {{ $t('settings.absolute_time_format_min_age') }} + </UnitSetting> + </li> + </ul> <h3>{{ $t('settings.attachments') }}</h3> <li> <BooleanSetting diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue index b5f49515..bf918441 100644 --- a/src/components/timeago/timeago.vue +++ b/src/components/timeago/timeago.vue @@ -3,7 +3,7 @@ :datetime="time" :title="localeDateString" > - {{ relativeTimeString }} + {{ relativeOrAbsoluteTimeString }} </time> </template> @@ -16,16 +16,28 @@ export default { props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold', 'templateKey'], data () { return { + relativeTimeMs: 0, relativeTime: { key: 'time.now', num: 0 }, interval: null } }, computed: { - localeDateString () { - const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) + shouldUseAbsoluteTimeFormat () { + if (!this.$store.getters.mergedConfig.useAbsoluteTimeFormat) { + return false + } + return DateUtils.durationStrToMs(this.$store.getters.mergedConfig.absoluteTimeFormatMinAge) <= this.relativeTimeMs + }, + browserLocale () { + return localeService.internalToBrowserLocale(this.$i18n.locale) + }, + timeAsDate () { return typeof this.time === 'string' - ? new Date(Date.parse(this.time)).toLocaleString(browserLocale) - : this.time.toLocaleString(browserLocale) + ? new Date(Date.parse(this.time)) + : this.time + }, + localeDateString () { + return this.timeAsDate.toLocaleString(this.browserLocale) }, relativeTimeString () { const timeString = this.$i18n.tc(this.relativeTime.key, this.relativeTime.num, [this.relativeTime.num]) @@ -35,6 +47,40 @@ export default { } return timeString + }, + absoluteTimeString () { + if (this.longFormat) { + return this.localeDateString + } + const now = new Date() + const formatter = (() => { + if (DateUtils.isSameDay(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + minute: 'numeric', + hour: 'numeric' + }) + } else if (DateUtils.isSameMonth(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + hour: 'numeric', + day: 'numeric' + }) + } else if (DateUtils.isSameYear(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + month: 'short', + day: 'numeric' + }) + } else { + return new Intl.DateTimeFormat(this.browserLocale, { + year: 'numeric', + month: 'short' + }) + } + })() + + return formatter.format(this.timeAsDate) + }, + relativeOrAbsoluteTimeString () { + return this.shouldUseAbsoluteTimeFormat ? this.absoluteTimeString : this.relativeTimeString } }, watch: { @@ -54,6 +100,7 @@ export default { methods: { refreshRelativeTimeObject () { const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1 + this.relativeTimeMs = DateUtils.relativeTimeMs(this.time) this.relativeTime = this.longFormat ? DateUtils.relativeTime(this.time, nowThreshold) : DateUtils.relativeTimeShort(this.time, nowThreshold) diff --git a/src/i18n/en.json b/src/i18n/en.json index 1b67d4f7..c1484bc5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -229,7 +229,9 @@ "expiry": "Poll age", "expires_in": "Poll ends in {0}", "expired": "Poll ended {0} ago", - "not_enough_options": "Too few unique options in poll" + "not_enough_options": "Too few unique options in poll", + "non_anonymous": "Public poll", + "non_anonymous_title": "Other instances may display the options you voted for" }, "emoji": { "stickers": "Stickers", @@ -506,6 +508,8 @@ "autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available", "emoji_reactions_on_timeline": "Show emoji reactions on timeline", "emoji_reactions_scale": "Reactions scale factor", + "absolute_time_format": "Use absolute time format", + "absolute_time_format_min_age": "Only use for time older than this amount of time", "export_theme": "Save preset", "filtering": "Filtering", "wordfilter": "Wordfilter", diff --git a/src/i18n/nan-TW.json b/src/i18n/nan-TW.json index 782a75f5..44a693dd 100644 --- a/src/i18n/nan-TW.json +++ b/src/i18n/nan-TW.json @@ -190,7 +190,8 @@ "mobile_notifications_close": "關掉通知", "announcements": "公告", "search": "Tshuē", - "mobile_notifications_mark_as_seen": "Lóng 標做有讀" + "mobile_notifications_mark_as_seen": "Lóng 標做有讀", + "quotes": "引用" }, "notifications": { "broken_favorite": "狀態毋知影,leh tshiau-tshuē……", @@ -212,7 +213,8 @@ "unread_follow_requests": "{num}ê新ê跟tuè請求", "configuration_tip": "用{theSettings},lí通自訂siánn物佇tsia顯示。{dismiss}", "configuration_tip_settings": "設定", - "configuration_tip_dismiss": "Mài koh顯示" + "configuration_tip_dismiss": "Mài koh顯示", + "subscribed_status": "有發送ê" }, "polls": { "add_poll": "開投票", @@ -252,7 +254,8 @@ }, "load_all_hint": "載入頭前 {saneAmount} ê 繪文字,規个攏載入效能可能 ē khah 食力。", "load_all": "Kā {emojiAmount} ê 繪文字攏載入", - "regional_indicator": "地區指引 {letter}" + "regional_indicator": "地區指引 {letter}", + "hide_custom_emoji": "Khàm掉自訂ê繪文字" }, "errors": { "storage_unavailable": "Pleroma buē-tàng the̍h 著瀏覽器儲存 ê。Lí ê 登入狀態抑是局部設定 buē 儲存,mā 凡勢 tú 著意料外 ê 問題。拍開 cookie 看māi。" @@ -263,7 +266,8 @@ "emoji_reactions": "繪文字 ê 反應", "reports": "檢舉", "moves": "用者 ê 移民", - "load_older": "載入 koh khah 早 ê 互動" + "load_older": "載入 koh khah 早 ê 互動", + "statuses": "訂ê" }, "post_status": { "edit_status": "編輯狀態", @@ -935,7 +939,34 @@ "notification_extra_chats": "顯示bô讀ê開講", "notification_extra_announcements": "顯示bô讀ê公告", "notification_extra_follow_requests": "顯示新ê跟tuè請求", - "notification_extra_tip": "顯示自訂其他通知ê撇步" + "notification_extra_tip": "顯示自訂其他通知ê撇步", + "confirm_new_setting": "Lí敢確認新ê設定?", + "text_size_tip": "用 {0} 做絕對值,{1} ē根據瀏覽器ê標準文字sài-suh放大縮小。", + "theme_debug": "佇處理透明ê時,顯示背景主題ia̋n-jín 所假使ê(DEBUG)", + "units": { + "time": { + "s": "秒鐘", + "m": "分鐘", + "h": "點鐘", + "d": "工" + } + }, + "actor_type": "Tsit ê口座是:", + "actor_type_Person": "一般ê用者", + "actor_type_description": "標記lí ê口座做群組,ē hōo自動轉送提起伊ê狀態。", + "actor_type_Group": "群組", + "actor_type_Service": "機器lâng", + "appearance": "外觀", + "confirm_new_question": "Tse看起來kám好?設定ē佇10秒鐘後改轉去。", + "revert": "改轉去", + "confirm": "確認", + "text_size": "文字kap界面ê sài-suh", + "text_size_tip2": "毋是 {0} ê值可能ē破壞一寡物件kap主題", + "emoji_size": "繪文字ê sài-suh", + "navbar_size": "頂 liâu-á êsài-suh", + "panel_header_size": "面pang標題ê sài-suh", + "visual_tweaks": "細細ê外觀調整", + "scale_and_layout": "界面ê sài-suh kap排列" }, "status": { "favorites": "收藏", @@ -1001,7 +1032,7 @@ "show_only_conversation_under_this": "Kan-ta顯示tsit ê狀態ê回應", "status_history": "狀態ê歷史", "reaction_count_label": "{num}ê lâng用表情反應", - "hide_quote": "Khàm條引用ê狀態", + "hide_quote": "Khàm掉引用ê狀態", "display_quote": "顯示引用ê狀態", "invisible_quote": "引用ê狀態bē當用:{link}", "more_actions": "佇tsit ê狀態ê其他動作" diff --git a/src/modules/config.js b/src/modules/config.js index cf84234a..835dcce4 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -180,7 +180,9 @@ export const defaultState = { autocompleteSelect: undefined, // instance default closingDrawerMarksAsSeen: undefined, // instance default unseenAtTop: undefined, // instance default - ignoreInactionableSeen: undefined // instance default + ignoreInactionableSeen: undefined, // instance default + useAbsoluteTimeFormat: undefined, // instance defualt + absoluteTimeFormatMinAge: undefined // instance default } // caching the instance default properties diff --git a/src/modules/instance.js b/src/modules/instance.js index 99b8b5d5..994f60a5 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -119,6 +119,8 @@ const defaultState = { closingDrawerMarksAsSeen: true, unseenAtTop: false, ignoreInactionableSeen: false, + useAbsoluteTimeFormat: false, + absoluteTimeFormatMinAge: '0d', // Nasty stuff customEmoji: [], diff --git a/src/services/date_utils/date_utils.js b/src/services/date_utils/date_utils.js index ed8e1417..69398c0c 100644 --- a/src/services/date_utils/date_utils.js +++ b/src/services/date_utils/date_utils.js @@ -6,10 +6,13 @@ export const WEEK = 7 * DAY export const MONTH = 30 * DAY export const YEAR = 365.25 * DAY -export const relativeTime = (date, nowThreshold = 1) => { +export const relativeTimeMs = (date) => { if (typeof date === 'string') date = Date.parse(date) + return Math.abs(Date.now() - date) +} +export const relativeTime = (date, nowThreshold = 1) => { const round = Date.now() > date ? Math.floor : Math.ceil - const d = Math.abs(Date.now() - date) + const d = relativeTimeMs(date) const r = { num: round(d / YEAR), key: 'time.unit.years' } if (d < nowThreshold * SECOND) { r.num = 0 @@ -57,3 +60,39 @@ export const secondsToUnit = (unit, amount) => { case 'days': return (1000 * amount) / DAY } } + +export const isSameYear = (a, b) => { + return a.getFullYear() === b.getFullYear() +} + +export const isSameMonth = (a, b) => { + return a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() +} + +export const isSameDay = (a, b) => { + return a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() +} + +export const durationStrToMs = (str) => { + if (typeof str !== 'string') { + return 0 + } + + const unit = str.replace(/[0-9,.]+/, '') + const value = str.replace(/[^0-9,.]+/, '') + switch (unit) { + case 'd': + return value * DAY + case 'h': + return value * HOUR + case 'm': + return value * MINUTE + case 's': + return value * SECOND + default: + return 0 + } +} diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js index 1b108b85..9a96cf01 100644 --- a/src/services/locale/locale.service.js +++ b/src/services/locale/locale.service.js @@ -19,7 +19,7 @@ const internalToBackendLocaleMulti = codes => { const getLanguageName = (code) => { const specialLanguageNames = { - pdc: 'Pennsylvania Dutch', + pdc: 'Pennsilfaanisch-Deitsch', ja_easy: 'やさしいにほんご', 'nan-TW': '臺語(閩南語)', zh: '简体中文', diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js new file mode 100644 index 00000000..909e9411 --- /dev/null +++ b/src/services/theme_data/iss_deserializer.js @@ -0,0 +1,157 @@ +import { flattenDeep } from 'lodash' + +const parseShadow = string => { + const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha'] + const regexPrep = [ + // inset keyword (optional) + '^(?:(inset)\\s+)?', + // x + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)', + // y + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)', + // blur (optional) + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?', + // spread (optional) + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?', + // either hex, variable or function + '(#[0-9a-f]{6}|--[a-z\\-_]+|\\$[a-z\\-()_]+)', + // opacity (optional) + '(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?$' + ].join('') + const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string + const result = regex.exec(string) + if (result == null) { + return string + } else { + const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha']) + const { x, y, blur, spread, alpha, inset, color } = Object.fromEntries(modes.map((mode, i) => { + if (numeric.has(mode)) { + return [mode, Number(result[i])] + } else if (mode === 'inset') { + return [mode, !!result[i]] + } else { + return [mode, result[i]] + } + }).filter(([k, v]) => v !== false).slice(1)) + + return { x, y, blur, spread, color, alpha, inset } + } +} +// this works nearly the same as HTML tree converter +const parseIss = (input) => { + const buffer = [{ selector: null, content: [] }] + let textBuffer = '' + + const getCurrentBuffer = () => { + let current = buffer[buffer.length - 1] + if (current == null) { + current = { selector: null, content: [] } + } + return current + } + + // Processes current line buffer, adds it to output buffer and clears line buffer + const flushText = (kind) => { + if (textBuffer === '') return + if (kind === 'content') { + getCurrentBuffer().content.push(textBuffer.trim()) + } else { + getCurrentBuffer().selector = textBuffer.trim() + } + textBuffer = '' + } + + for (let i = 0; i < input.length; i++) { + const char = input[i] + + if (char === ';') { + flushText('content') + } else if (char === '{') { + flushText('header') + } else if (char === '}') { + flushText('content') + buffer.push({ selector: null, content: [] }) + textBuffer = '' + } else { + textBuffer += char + } + } + + return buffer +} +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 + + 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('') + } + 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 (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 => parseShadow(v.trim())) + } + } if (!Number.isNaN(Number(value))) { + realValue = Number(value) + } + return [property, realValue] + })) + + return output + }) + return result + }) + return flattenDeep(finalResult) +} diff --git a/src/services/theme_data/iss_serializer.js b/src/services/theme_data/iss_serializer.js new file mode 100644 index 00000000..8b7cf5d8 --- /dev/null +++ b/src/services/theme_data/iss_serializer.js @@ -0,0 +1,48 @@ +import { unroll } from './iss_utils.js' + +const serializeShadow = s => { + if (typeof s === 'object') { + return `${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}` + } else { + return s + } +} + +export const serialize = (ruleset) => { + 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') + + 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` + } + default: + return ` ${directive}: ${value}` + } + } + }) + + return `${header} {\n${content.join(';\n')}\n}` + }).filter(x => x).join('\n\n') +} diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 8a0c170b..3c2f8a63 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -522,9 +522,21 @@ export const init = ({ console.debug('Eager processing took ' + (t2 - t1) + ' ms') } + // optimization to traverse big-ass array only once instead of twice + const eager = [] + const lazy = [] + + result.forEach(x => { + if (typeof x === 'function') { + lazy.push(x) + } else { + eager.push(x) + } + }) + return { - lazy: result.filter(x => typeof x === 'function'), - eager: result.filter(x => typeof x !== 'function'), + lazy, + eager, staticVars, engineChecksum } |
