diff options
Diffstat (limited to 'src')
30 files changed, 692 insertions, 95 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js index a486bd4c..6cad05f6 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -122,6 +122,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { store.dispatch('setInstanceOption', { name, value: config[name] }) } + copyInstanceOption('theme') copyInstanceOption('nsfwCensorImage') copyInstanceOption('background') copyInstanceOption('hidePostStats') 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 6fec67a0..1bee8f8e 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/button_unstyled.style.js b/src/components/button_unstyled.style.js index 65b5c57b..435f9cc6 100644 --- a/src/components/button_unstyled.style.js +++ b/src/components/button_unstyled.style.js @@ -16,8 +16,7 @@ export default { { directives: { background: '#ffffff', - opacity: 0, - shadow: [] + opacity: 0 } }, { diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index d71bc1bb..9ea5c877 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -150,7 +150,9 @@ const EmojiPicker = { }, showPicker () { this.$refs.popover.showPopover() - this.onShowing() + this.$nextTick(() => { + this.onShowing() + }) }, hidePicker () { this.$refs.popover.hidePopover() diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index 7c36deaa..a3dc8f9e 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -89,6 +89,7 @@ class="emoji-groups" :class="groupsScrolledClass" :min-item-size="minItemSize" + :buffer="minItemSize" :items="emojiItems" :emit-update="true" @update="onScroll" diff --git a/src/components/input.style.js b/src/components/input.style.js index 139a0034..7302cd6d 100644 --- a/src/components/input.style.js +++ b/src/components/input.style.js @@ -26,7 +26,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/panel_header.style.js b/src/components/panel_header.style.js index 32464bc5..010e42cd 100644 --- a/src/components/panel_header.style.js +++ b/src/components/panel_header.style.js @@ -16,8 +16,7 @@ export default { component: 'PanelHeader', directives: { backgroundNoCssColor: 'yes', - background: '--fg', - shadow: [] + background: '--fg' } } ] 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/components/timeline/timeline.scss b/src/components/timeline/timeline.scss index 0fc0d979..2dd66328 100644 --- a/src/components/timeline/timeline.scss +++ b/src/components/timeline/timeline.scss @@ -26,7 +26,7 @@ } .conversation-heading { - top: calc(var(--__panel-heading-height) * var(--currentPanelStack, 2)); + top: calc(var(--__panel-heading-height) * var(--currentPanelStack, 1) + var(--navbar-height)); z-index: 2; } diff --git a/src/i18n/en.json b/src/i18n/en.json index 3f7ea282..423ce65e 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/eo.json b/src/i18n/eo.json index 34a506b0..2912fc99 100644 --- a/src/i18n/eo.json +++ b/src/i18n/eo.json @@ -475,7 +475,8 @@ "interface": "Fasado", "input": "Enigaj kampoj", "post": "Teksto de afiŝo", - "postCode": "Egallarĝa teksto en afiŝo (riĉteksto)" + "postCode": "Egallarĝa teksto en afiŝo (riĉteksto)", + "monospace": "Egallarĝa teksto" }, "family": "Nomo de tiparo", "size": "Grando (en bilderoj)", @@ -495,6 +496,27 @@ "header_faint": "Tio estas en ordo", "checkbox": "Mi legetis la kondiĉojn de uzado", "link": "bela eta ligil’" + }, + "custom_theme_used": "(Propra haŭto)", + "themes3": { + "hacks": { + "underlay_override_mode_transparent": "Tute forigi (povus rompi iujn haŭtojn)", + "forced_roundness_mode_disabled": "Uzi implicitajn valorojn de haŭto", + "forced_roundness_mode_sharp": "Devigi akrajn randojn", + "forced_roundness_mode_nonsharp": "Devigi ne tiom akrajn randojn (rondigo je 1 bildero)", + "forced_roundness_mode_round": "Devigi rondajn randojn" + }, + "font": { + "builtin": { + "serif": "Kalkana", + "sans-serif": "Senkalkana", + "monospace": "Egallarĝa", + "inherit": "Senŝanĝe" + }, + "group-local": "Loke instalitaj signoformoj", + "local-unavailable1": "Listo de loke instalitaj signoformoj ne estas disponebla", + "font_list_unavailable": "Ne povis akiri loke instalitajn signoformojn: {error}" + } } }, "discoverable": "Permesi trovon de ĉi tiu konto en serĉrezultoj kaj aliaj servoj", @@ -744,7 +766,15 @@ "notification_visibility_reports": "Raportoj", "notification_setting_ignore_inactionable_seen": "Malatenti legitecon de nereageblaj sciigoj (ŝatoj, ripetoj, ktp.)", "notification_setting_ignore_inactionable_seen_tip": "Ĉi tio ne markos la sciigojn legitaj, kaj vi ankoraŭ ricevos labortablajn sciigojn pri ili, se vi elektis ricevi tiujn", - "notification_setting_unseen_at_top": "Montri nelegitajn sciigojn super aliaj" + "notification_setting_unseen_at_top": "Montri nelegitajn sciigojn super aliaj", + "appearance": "Aspekto", + "confirm_new_setting": "Ĉu konfirmi novan agordon?", + "confirm_new_question": "Ĉu tio ĉi aspektas ĝuste? La ŝanĝo malfariĝos post 10 sekundoj.", + "revert": "Malfari", + "confirm": "Konfirmi", + "text_size": "Grandeco de teksto kaj fasado", + "emoji_size": "Grandeco de bildosignoj", + "navbar_size": "Grandeco de supra breto" }, "timeline": { "collapse": "Maletendi", @@ -999,7 +1029,8 @@ "follows": "Novaj abonoj", "favs_repeats": "Ripetoj kaj ŝatoj", "emoji_reactions": "Bildosignaj reagoj", - "reports": "Raportoj" + "reports": "Raportoj", + "statuses": "Abonoj" }, "errors": { "storage_unavailable": "Pleroma ne povis aliri deponejon de la foliumilo. Via saluto kaj viaj lokaj agordoj ne estos konservitaj, kaj vi eble renkontos neatenditajn problemojn. Provu permesi kuketojn." @@ -1065,7 +1096,13 @@ "repeat_confirm_title": "Konfirmo de ripeto", "repeat_confirm_accept_button": "Ripeti", "repeat_confirm_cancel_button": "Ne ripeti", - "delete_confirm_cancel_button": "Ne forigi" + "delete_confirm_cancel_button": "Ne forigi", + "delete_error": "Eraris forigo de afiŝo: {0}", + "hide_quote": "Kaŝi la cititan afiŝon", + "display_quote": "Montri la cititan afiŝon", + "reaction_count_label": "{num} persono reagis | {num} personoj reagis", + "invisible_quote": "Citita afiŝo ne disponeblas: {link}", + "quotes": "Citaĵoj" }, "time": { "years_short": "{0}j", @@ -1267,7 +1304,9 @@ "save_meta": "Konservi pridatumojn", "description": "Priskribo", "homepage": "Hejmpaĝo", - "save": "Konservi" + "save": "Konservi", + "revert": "Malfari", + "share": "Kunhavigi" }, "tabs": { "emoji": "Bildosignoj", @@ -1283,7 +1322,8 @@ "header": "Limigi aliron por sennomaj vizitantoj", "timelines": "Aliro al historioj" }, - "access": "Aliro al nodo" + "access": "Aliro al nodo", + "kocaptcha": "Agordo de KoCaptcha" }, "limits": { "users": "Limoj de profiloj de uzantoj", @@ -1304,10 +1344,21 @@ ":instance": { ":public": { "label": "Nodo estas publika" + }, + ":background_image": { + "label": "Fonbildo", + "description": "Fonbildo (uzota ĉefe de PleromaFE)" + }, + ":description_limit": { + "description": "Limo de signoj por priskriboj de kunsendaĵoj", + "label": "Limo" } } } }, - "commit_all": "Konservi ĉion" + "commit_all": "Konservi ĉion", + "captcha": { + "kocaptcha": "KoCaptcha" + } } } diff --git a/src/i18n/fr.json b/src/i18n/fr.json index e2396937..4d421ff1 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -131,7 +131,8 @@ "mobile_notifications_close": "Fermer les notifications", "search_close": "Fermer la barre de recherche", "announcements": "Annonces", - "mobile_notifications_mark_as_seen": "Marquer tout comme vu" + "mobile_notifications_mark_as_seen": "Marquer tout comme vu", + "quotes": "Citations" }, "notifications": { "broken_favorite": "Message inconnu, recherche en cours…", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index 45fd44e9..3d895fcd 100644 --- a/src/i18n/ja_pedantic.json +++ b/src/i18n/ja_pedantic.json @@ -565,7 +565,8 @@ "interface": "インターフェース", "input": "入力欄", "post": "投稿", - "postCode": "等幅 (投稿がリッチテキストであるとき)" + "postCode": "等幅 (投稿がリッチテキストであるとき)", + "monospace": "等幅テキスト" }, "family": "フォント名", "size": "大きさ (px)", @@ -585,7 +586,43 @@ "header_faint": "エラーではありません", "checkbox": "利用規約を読みました", "link": "ハイパーリンク" - } + }, + "themes2_outdated": "V2テーマのエディタは徐々に廃止され、最終的には新しいV3テーマのものに置き換えられる予定です。現状はまだ動作するはずですが、正しく動作する保証はありません。", + "themes3": { + "font": { + "group-local": "端末上にインストールされたフォント", + "local-unavailable2": "フォント名を直接指定してください", + "lookup_local_fonts": "端末上のフォントの一覧から選ぶ", + "group-builtin": "ブラウザのデフォルトフォント", + "builtin": { + "serif": "明朝体 (Serif)", + "sans-serif": "ゴシック体 (Sans-serif)", + "monospace": "等幅 (Monospace)", + "inherit": "変更しない" + }, + "local-unavailable1": "端末上のフォントの一覧が取得できません", + "font_list_unavailable": "端末上のフォントの一覧が取得できません: {error}", + "enter_manually": "フォント名を直接入力する", + "entry": "{fontFamily}を入力", + "select": "フォントを選択" + }, + "hacks": { + "underlay_overrides": "背景表示", + "underlay_override_mode_none": "テーマのデフォルトを使用する", + "underlay_override_mode_opaque": "単色に置き換える", + "underlay_override_mode_transparent": "非表示にする (テーマによっては表示が壊れる可能性があります)", + "force_interface_roundness": "インターフェースの角丸設定", + "forced_roundness_mode_disabled": "テーマのデフォルトを使用する", + "forced_roundness_mode_sharp": "角ばったデザインを強制する", + "forced_roundness_mode_nonsharp": "若干の角丸(1px分丸める)デザインを強制する", + "forced_roundness_mode_round": "角丸デザインを強制する" + }, + "define": "上書き" + }, + "custom_theme_used": "(カスタムテーマ)", + "appearance_tab_note": "以下の設定はテーマには反映されないため、エクスポートしたテーマの見た目は今見えているものと異なる可能性があります", + "update_preview": "プレビューを更新", + "interface_font_user_override": "フォント設定の上書き" }, "version": { "title": "バージョン", @@ -802,7 +839,19 @@ } }, "hide_scrobbles_after": "これより古いScrobbleを表示しない:", - "force_theme_recompilation_debug": "テーマのキャッシュを無効化し、起動の度にコンパイルし直す (デバッグ用)" + "force_theme_recompilation_debug": "テーマのキャッシュを無効化し、起動の度にコンパイルし直す (デバッグ用)", + "scale_and_layout": "インターフェースの表示サイズとレイアウト", + "appearance": "見た目", + "confirm_new_setting": "設定を適用しますか?", + "confirm_new_question": "これで問題ありませんか?10秒間操作がない場合、元の設定に戻ります。", + "revert": "元に戻す", + "confirm": "適用", + "text_size": "フォントサイズ", + "text_size_tip2": "{0}以外に設定すると見た目が壊れてしまう場合があります", + "emoji_size": "絵文字のサイズ", + "navbar_size": "トップバーのサイズ", + "panel_header_size": "パネルヘッダーのサイズ", + "notification_visibility_statuses": "購読" }, "time": { "day": "{0}日", @@ -941,7 +990,9 @@ "open_gallery": "メディアビューアで開く", "status_history": "編集履歴", "sensitive_muted": "閲覧注意な投稿のためミュートされています", - "load_error": "投稿の読み込みに失敗しました: {error}" + "load_error": "投稿の読み込みに失敗しました: {error}", + "loading": "読み込み中…", + "quotes": "引用" }, "user_card": { "approve": "承認", diff --git a/src/i18n/languages.js b/src/i18n/languages.js index 33a98f8e..475bf5ac 100644 --- a/src/i18n/languages.js +++ b/src/i18n/languages.js @@ -22,6 +22,7 @@ const languages = [ 'nl', 'oc', 'pl', + 'pdc', 'pt', 'ro', 'ru', diff --git a/src/i18n/pdc.json b/src/i18n/pdc.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/src/i18n/pdc.json @@ -0,0 +1 @@ +{} diff --git a/src/i18n/pl.json b/src/i18n/pl.json index efebcc83..9bfaa34d 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -24,7 +24,10 @@ "media_removal": "Usuwanie multimediów", "media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:", "media_nsfw": "Multimedia ustawione jako wrażliwe", - "media_nsfw_desc": "Ta instancja wymusza, by multimedia z wymienionych instancji były ustawione jako wrażliwe:" + "media_nsfw_desc": "Ta instancja wymusza, by multimedia z wymienionych instancji były ustawione jako wrażliwe:", + "instance": "Instancja", + "reason": "Powód", + "not_applicable": "Nie dotyczy" } }, "staff": "Administracja" @@ -861,5 +864,13 @@ }, "errors": { "storage_unavailable": "Pleroma nie mogła uzyskać dostępu do pamięci masowej przeglądarki. Twój login lub lokalne ustawienia nie zostaną zapisane i możesz napotkać problemy. Spróbuj włączyć ciasteczka." + }, + "announcements": { + "page_header": "Ogłoszenia", + "title": "Ogłoszenie", + "mark_as_read_action": "Oznacz jako przeczytane", + "post_placeholder": "Wprowadź treść ogłoszenia…", + "close_error": "Zamknij", + "delete_action": "Usuń" } } diff --git a/src/i18n/service_worker_messages.js b/src/i18n/service_worker_messages.js index f691f1c4..a4e6d60a 100644 --- a/src/i18n/service_worker_messages.js +++ b/src/i18n/service_worker_messages.js @@ -25,6 +25,7 @@ const messages = { oc: require('../lib/notification-i18n-loader.js!./oc.json'), pl: require('../lib/notification-i18n-loader.js!./pl.json'), pt: require('../lib/notification-i18n-loader.js!./pt.json'), + pdc: require('../lib/notification-i18n-loader.js!./pdc.json'), ro: require('../lib/notification-i18n-loader.js!./ro.json'), ru: require('../lib/notification-i18n-loader.js!./ru.json'), sk: require('../lib/notification-i18n-loader.js!./sk.json'), diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 2ade41f7..37ea03fc 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -130,20 +130,22 @@ "edit_nav_mobile": "自定义导航栏", "edit_pinned": "编辑固定的项目", "mobile_sidebar": "切换移动设备侧栏", - "search_close": "关闭搜索栏" + "search_close": "关闭搜索栏", + "mobile_notifications_mark_as_seen": "全部已阅", + "quotes": "引用" }, "notifications": { "broken_favorite": "未知的状态,正在搜索中…", - "favorited_you": "喜欢了你的状态", - "followed_you": "关注了你", + "favorited_you": "喜欢了您的状态", + "followed_you": "关注了您", "load_older": "加载更早的通知", "notifications": "通知", "read": "已阅!", - "repeated_you": "转发了你的状态", + "repeated_you": "转发了您的状态", "no_more_notifications": "没有更多的通知", - "reacted_with": "作出了 {0} 的反应", + "reacted_with": "作出了 {0} 的回应", "migrated_to": "迁移到了", - "follow_request": "想要关注你", + "follow_request": "想要关注您", "error": "取得通知时发生错误:{0}", "poll_ended": "投票结束了", "submitted_report": "提交举报", @@ -152,7 +154,8 @@ "unread_follow_requests": "{num} 个新关注请求", "configuration_tip": "可以在 {theSettings} 里定制什么会显示在这里。{dismiss}", "configuration_tip_settings": "设置", - "configuration_tip_dismiss": "不再显示" + "configuration_tip_dismiss": "不再显示", + "subscribed_status": "已发送" }, "polls": { "add_poll": "增加投票", @@ -179,11 +182,12 @@ "load_older": "加载更早的互动", "moves": "用户迁移", "reports": "举报", - "emoji_reactions": "表情回应" + "emoji_reactions": "表情回应", + "statuses": "订阅" }, "post_status": { "new_status": "发布新状态", - "account_not_locked_warning": "你的帐号没有 {0}。任何人都可以关注你并浏览你的上锁内容。", + "account_not_locked_warning": "您的帐号没有 {0}。任何人都可以关注您并浏览您的上锁内容。", "account_not_locked_warning_link": "上锁", "attachments_sensitive": "标记附件为敏感内容", "content_type": { @@ -199,12 +203,12 @@ "posting": "发送中", "scope_notice": { "public": "本条内容可以被所有人看到", - "private": "关注你的人才能看到本条内容", + "private": "关注您的人才能看到本条内容", "unlisted": "本条内容既不在公共时间线,也不会在所有已知网络上可见" }, "scope": { "direct": "私信 - 只发送给被提及的用户", - "private": "仅关注者 - 只有关注了你的人能看到", + "private": "仅关注者 - 只有关注了您的人能看到", "public": "公共 - 发送到公共时间轴", "unlisted": "不公开 - 不会发送到公共时间轴" }, @@ -212,7 +216,7 @@ "preview": "预览", "media_description": "媒体描述", "media_description_error": "更新媒体失败,请重试", - "empty_status_error": "不能发布没有内容、没有附件的发文", + "empty_status_error": "不能发布没有内容、没有附件的帖子", "post": "发送", "edit_remote_warning": "其它远程实例可能不支持编辑并且无法接收您的帖子的最新版本。", "edit_unsupported_warning": "Pleroma 不支持对提及或投票进行编辑。", @@ -233,7 +237,7 @@ "new_captcha": "点击图片获取新的验证码", "username_placeholder": "例如:lain", "fullname_placeholder": "例如:岩仓玲音", - "bio_placeholder": "例如:\n你好,我是玲音。\n我是一个住在日本郊区的动画少女。你可能在 Wired 见过我。", + "bio_placeholder": "例如:\n你好,我是玲音。\n我是一个住在日本郊区的动画少女。您可能在 Wired 见过我。", "validations": { "username_required": "不能留空", "fullname_required": "不能留空", @@ -247,7 +251,7 @@ "reason_placeholder": "此实例的注册需要手动批准。\n请让管理员知道您为什么想要注册。", "reason": "注册理由", "register": "注册", - "email_language": "你想从服务器收到什么语言的邮件?", + "email_language": "您想从服务器收到什么语言的邮件?", "bio_optional": "介绍(可选)", "email_optional": "电子邮件(可选)", "birthday": "生日:", @@ -289,7 +293,7 @@ "background": "背景", "bio": "简介", "block_export": "屏蔽名单导出", - "block_export_button": "导出你的屏蔽名单到一个 csv 文件", + "block_export_button": "导出您的屏蔽名单到一个 csv 文件", "block_import": "屏蔽名单导入", "block_import_error": "导入屏蔽名单出错", "blocks_imported": "屏蔽名单导入成功!需要一点时间来处理。", @@ -310,10 +314,10 @@ "current_profile_banner": "您当前的横幅图片", "data_import_export_tab": "数据导入/导出", "default_vis": "默认可见范围", - "delete_account": "删除账户", - "delete_account_description": "永久删除你的帐号和所有数据。", - "delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。", - "delete_account_instructions": "在下面输入您的密码来确认删除账户。", + "delete_account": "删除账号", + "delete_account_description": "永久删除您的帐号和所有数据。", + "delete_account_error": "删除账号时发生错误。如果一直删除不了,请联系实例管理员。", + "delete_account_instructions": "在下面输入您的密码来确认删除账号。", "avatar_size_instruction": "推荐的头像图片最小尺寸为 150x150 像素。", "export_theme": "导出预置主题", "filtering": "过滤器", @@ -388,11 +392,11 @@ "autohide_floating_post_button": "自动隐藏新帖子的按钮(移动设备)", "saving_err": "保存设置时发生错误", "saving_ok": "设置已保存", - "search_user_to_block": "搜索你想屏蔽的用户", - "search_user_to_mute": "搜索你想要隐藏的用户", + "search_user_to_block": "搜索您想屏蔽的用户", + "search_user_to_mute": "搜索您想要隐藏的用户", "security_tab": "安全", "scope_copy": "回复时复制可见范围(私信中永远会复制)", - "minimal_scopes_mode": "使发文可见范围的选项最少化", + "minimal_scopes_mode": "使帖子可见范围的选项最小化", "set_new_avatar": "设置新头像", "set_new_profile_background": "设置新的个人资料背景", "set_new_profile_banner": "设置新的横幅图片", @@ -402,7 +406,7 @@ "subject_line_email": "类似电子邮件: \"re: 主题\"", "subject_line_mastodon": "类似 mastodon: 与原主题相同", "subject_line_noop": "不要复制", - "post_status_content_type": "发文状态内容类型", + "post_status_content_type": "帖子状态内容类型", "stop_gifs": "鼠标悬停时播放GIF", "streaming": "滚动到顶部时自动推送新内容", "text": "文本", @@ -546,7 +550,8 @@ "interface": "界面", "input": "输入框", "post": "发帖文字", - "postCode": "帖子中使用等间距文字(富文本)" + "postCode": "帖子中使用等间距文字(富文本)", + "monospace": "等宽文本" }, "family": "字体名称", "size": "大小 (in px)", @@ -566,7 +571,43 @@ "header_faint": "这很正常", "checkbox": "我已经浏览了条款及细则", "link": "一个棒棒的小小链接" - } + }, + "custom_theme_used": "(自定义主题)", + "themes2_outdated": "V2 主题的编辑器正在被淘汰并且最终会被新的利用 V3 主题引擎的编辑器取代。但是体验有可能会被降级并且不稳定。", + "appearance_tab_note": "在这个标签页的更改不会影响使用的主题,所以导出的主题会和界面显示的主题不同", + "update_preview": "更新预览", + "themes3": { + "define": "覆盖", + "hacks": { + "underlay_overrides": "更改底色", + "underlay_override_mode_none": "主题默认", + "underlay_override_mode_opaque": "使用单色更改", + "underlay_override_mode_transparent": "完全移除(有可能破外一些主题)", + "force_interface_roundness": "覆盖界面圆角/锐度", + "forced_roundness_mode_disabled": "使用主题默认", + "forced_roundness_mode_sharp": "强制使用锐利边角", + "forced_roundness_mode_nonsharp": "强制使用不太锋利(1px 圆角)的边角", + "forced_roundness_mode_round": "强制使用圆角" + }, + "font": { + "group-builtin": "浏览器默认字体", + "builtin": { + "serif": "衬线字体", + "sans-serif": "无衬线字体", + "monospace": "等宽字体", + "inherit": "未更改" + }, + "group-local": "本地字体", + "local-unavailable1": "不可用的本地字体列表", + "local-unavailable2": "使用手动输入来指定自定义字体", + "font_list_unavailable": "无法找到本地字体:{error}", + "lookup_local_fonts": "加载这台电脑的本地字体列表", + "enter_manually": "手动输入字体名称", + "entry": "输入 {fontFamily}", + "select": "选择字体" + } + }, + "interface_font_user_override": "覆盖使用的主题/浏览器字体" }, "version": { "title": "版本", @@ -582,12 +623,12 @@ "notification_setting_privacy_option": "在通知推送中隐藏发送者和内容", "notification_setting_privacy": "隐私", "hide_follows_count_description": "不显示关注数", - "notification_visibility_emoji_reactions": "互动", + "notification_visibility_emoji_reactions": "回应", "notification_visibility_moves": "用户迁移", "new_email": "新邮箱", - "emoji_reactions_on_timeline": "在时间线上显示表情符号互动", + "emoji_reactions_on_timeline": "在时间线上显示表情符号回应", "notification_setting_hide_notification_contents": "隐藏推送通知中的发送者与内容信息", - "notification_setting_block_from_strangers": "屏蔽来自你没有关注的用户的通知", + "notification_setting_block_from_strangers": "屏蔽来自您没有关注的用户的通知", "type_domains_to_mute": "搜索需要隐藏的域名", "useStreamingApi": "实时接收帖子和通知", "user_mutes": "用户", @@ -618,15 +659,15 @@ "mutes_imported": "隐藏名单导入成功!处理它们将需要一段时间。", "mute_import_error": "导入隐藏名单出错", "mute_import": "隐藏名单导入", - "mute_export_button": "导出你的隐藏名单到一个 csv 文件", + "mute_export_button": "导出您的隐藏名单到一个 csv 文件", "mute_export": "隐藏名单导出", "hide_wallpaper": "隐藏实例壁纸", "setting_changed": "与默认设置不同", "more_settings": "更多设置", - "sensitive_by_default": "默认标记发文为敏感内容", + "sensitive_by_default": "默认标记帖子为敏感内容", "reply_visibility_self_short": "只显示对我本人的回复", "reply_visibility_following_short": "显示对我关注的人的回复", - "hide_all_muted_posts": "不显示已隐藏的发文", + "hide_all_muted_posts": "不显示已隐藏的帖子", "hide_media_previews": "隐藏媒体预览", "word_filter": "词语过滤", "save": "保存更改", @@ -664,14 +705,14 @@ "move_account_target": "目标账号(例如 {example})", "moved_account": "账号移动好了。", "move_account_error": "移动账号时出错:{error}", - "setting_server_side": "这个设置是捆绑到你的个人资料的,能影响所有会话和客户端", + "setting_server_side": "这个设置是捆绑到您的个人资料的,能影响所有会话和客户端", "post_look_feel": "文章的样子跟感受", "email_language": "从服务器收邮件的语言", - "account_backup_description": "这个允许你下载一份账号信息和文章的存档,但是现在还不能导入到 Pleroma 账号里。", + "account_backup_description": "这个允许您下载一份账号信息和文章的存档,但是现在还不能导入到 Pleroma 账号里。", "backup_not_ready": "备份还没准备好。", "add_backup_error": "添加新备份时出错:{error}", "add_alias_error": "添加别名时出错:{error}", - "move_account_notes": "如果你想把账号移动到别的地方,你必须去目标账号,然后加一个指向这里的别名。", + "move_account_notes": "如果您想把账号移动到别的地方,您必须去目标账号,然后加一个指向这里的别名。", "wordfilter": "词语过滤器", "user_profiles": "用户资料", "third_column_mode_notifications": "通知栏", @@ -685,7 +726,7 @@ }, "hide_favorites_description": "不显示我的喜欢列表(人们仍然会收到通知)", "third_column_mode": "当有足够的空间时,显示第三栏包含", - "third_column_mode_postform": "主要的发文形式和导航", + "third_column_mode_postform": "主要的帖子形式和导航", "columns": "分栏", "user_popover_avatar_overlay": "在用户头像上显示用户弹出窗口", "navbar_column_stretch": "延伸导航栏至分栏宽度", @@ -705,12 +746,12 @@ "max_depth_in_thread": "默认显示同主题帖子中的最大层数", "hide_wordfiltered_statuses": "隐藏经过词语过滤的状态", "hide_muted_threads": "不显示已隐藏的同主题帖子", - "notification_visibility_polls": "你所投的投票的结束于", + "notification_visibility_polls": "您所投的投票的结束于", "tree_advanced": "允许在树状视图中进行更灵活的导航", "tree_fade_ancestors": "以模糊的文字显示当前状态的上级", "conversation_display_linear": "线性样式", "mention_link_fade_domain": "淡化域名(例如:{'@'}example.org 中的 {'@'}foo{'@'}example.org)", - "mention_link_bolden_you": "当你被提及时突出显示提及你", + "mention_link_bolden_you": "当您被提及时突出显示提及您", "user_popover_avatar_action": "弹出式头像点击动作", "user_popover_avatar_action_zoom": "缩放头像", "user_popover_avatar_action_close": "关闭弹出窗口", @@ -750,7 +791,7 @@ "url": "URL", "preview": "预览", "commit_value": "保存", - "commit_value_tooltip": "当前值未保存,请按此按钮以提交你的修改", + "commit_value_tooltip": "当前值未保存,请按此按钮以提交您的修改", "reset_value": "重置", "reset_value_tooltip": "重置草稿", "hard_reset_value": "硬重置", @@ -760,7 +801,52 @@ "notification_extra_chats": "显示未读聊天", "notification_extra_announcements": "显示未读公告", "notification_extra_follow_requests": "显示新的关注请求", - "notification_extra_tip": "显示额外通知的定制提示" + "notification_extra_tip": "显示额外通知的定制提示", + "notification_visibility_follow_requests": "关注请求", + "notification_visibility_reports": "举报", + "mute_sensitive_posts": "隐藏敏感帖子", + "notification_visibility_in_column": "在侧栏/菜单显示通知菜单", + "notification_visibility_native_notifications": "显示本地通知", + "units": { + "time": { + "m": "分钟", + "s": "秒", + "h": "小时", + "d": "天" + } + }, + "hide_scrobbles_after": "隐藏比这个时间更早的 scrobble", + "notification_setting_ignore_inactionable_seen": "忽略无法回复通知(喜欢,转发等)的已阅状态", + "notification_setting_unseen_at_top": "将未读通知置顶", + "notification_setting_ignore_inactionable_seen_tip": "如果您继续,这将不会标记这些通知为已读,并且您仍会接收到桌面推送通知", + "actor_type": "账号:", + "actor_type_description": "将您的账号标记为组会使其转发所有提及它的状态。", + "actor_type_Person": "正常用户", + "actor_type_Service": "机器人", + "actor_type_Group": "组", + "hide_actor_type_indication": "隐藏帖子中账号类型(机器人,组等)的表示", + "notification_setting_annoyance": "烦扰", + "notification_setting_drawer_marks_as_seen": "关闭菜单(移动端)来标记全部通知为已阅", + "enable_web_push_always_show_tip": "一些浏览器(Chromium,Chrome)需要推送信息才能显示通知,否则会显示“网页在背景发生了更改”的通知,勾选这个选项可以防止这种通知显示,因为 Chrome 在标签页激活时会隐藏网页推送通知。可能会在其他浏览器中显示双重通知。", + "enable_web_push_always_show": "总是显示网页推送通知", + "force_theme_recompilation_debug": "禁用主题缓存,强制在每次启动时重新编译(调试)", + "notification_setting_filters_chrome_push": "在一些浏览器中(Chrome),有可能无法完全按照类型过滤通过推送传递的通知", + "hide_scrobbles": "隐藏 scrobble", + "appearance": "外观", + "confirm_new_setting": "确认新的设置?", + "confirm_new_question": "是否保留这些设置?设置将在 10 秒后还原。", + "revert": "恢复", + "confirm": "确定", + "text_size": "文字与界面大小", + "text_size_tip": "用 {0} 作为绝对值,{1} 会根据浏览器默认文字大小进行缩放。", + "text_size_tip2": "{0} 之外的值可能会破坏一些功能和主题", + "emoji_size": "表情符号大小", + "navbar_size": "顶栏大小", + "panel_header_size": "面板标题大小", + "visual_tweaks": "细微外观调整", + "theme_debug": "显示当遇到透明背景时背景主题引擎的假设(调试)", + "scale_and_layout": "界面大小与布局", + "notification_visibility_statuses": "订阅" }, "time": { "day": "{0} 天", @@ -856,7 +942,7 @@ "nsfw": "NSFW", "external_source": "外部来源", "expand": "展开", - "you": "(你)", + "you": "(您)", "plus_more": "还有 {number} 个", "many_attachments": "文章有 {number} 个附件", "collapse_attachments": "折起附件", @@ -896,7 +982,12 @@ "reaction_count_label": "{num} 人作出了表情回应", "invisible_quote": "引用的状态不可用:{link}", "hide_quote": "隐藏引用的状态", - "display_quote": "显示引用的状态" + "display_quote": "显示引用的状态", + "quotes": "引用", + "sensitive_muted": "正在隐藏敏感内容", + "loading": "加载中...", + "load_error": "无法加载动态:{error}", + "more_actions": "状态的更多动作" }, "user_card": { "approve": "核准", @@ -911,14 +1002,14 @@ "followees": "正在关注", "followers": "关注者", "following": "正在关注!", - "follows_you": "关注了你!", - "its_you": "就是你!", + "follows_you": "关注了您!", + "its_you": "就是您!", "media": "媒体", "mute": "隐藏", "muted": "已隐藏", "per_day": "每天", "remote_follow": "跨站关注", - "report": "报告", + "report": "举报", "statuses": "状态", "subscribe": "订阅", "unsubscribe": "退订", @@ -945,7 +1036,7 @@ "disable_any_subscription": "完全禁止关注用户", "quarantine": "不许帖子传入别站", "delete_user": "删除用户", - "delete_user_data_and_deactivate_confirmation": "这将永久删除该账户的数据并停用该账户。你完全确定吗?" + "delete_user_data_and_deactivate_confirmation": "这将永久删除该账号的数据并停用该账号。您完全确定吗?" }, "hidden": "已隐藏", "show_repeats": "显示转发", @@ -993,7 +1084,8 @@ "note_blank": "(空)", "edit_note": "编辑备注", "edit_note_apply": "应用", - "edit_note_cancel": "取消" + "edit_note_cancel": "取消", + "group": "组" }, "user_profile": { "timeline_title": "用户时间线", @@ -1001,10 +1093,10 @@ "profile_loading_error": "抱歉,载入个人资料时出错。" }, "user_reporting": { - "title": "报告 {0}", - "add_comment_description": "此报告会发送给您的实例监察员。您可以在下面提供更多详细信息解释报告的缘由:", + "title": "举报 {0}", + "add_comment_description": "此举报会发送给您的实例监察员。您可以在下面提供更多详细信息解释举报的缘由:", "additional_comments": "其它信息", - "forward_description": "这个账号来自另一个服务器。是否同时发送一份报告副本到那里?", + "forward_description": "这个账号来自另一个服务器。是否同时发送一份举报副本到那里?", "forward_to": "转发 {0}", "submit": "提交", "generic_error": "当处理您的请求时,发生了一个错误。" @@ -1020,7 +1112,7 @@ "favorite": "喜欢", "user_settings": "用户设置", "reject_follow_request": "拒绝关注请求", - "add_reaction": "添加互动", + "add_reaction": "添加回应", "bookmark": "书签", "accept_follow_request": "接受关注请求", "toggle_expand": "展开或折叠通知以显示帖子全文", @@ -1090,7 +1182,8 @@ "smileys-and-emotion": "表情与情感" }, "regional_indicator": "地区指示符 {letter}", - "unpacked": "未分组的表情符号" + "unpacked": "未分组的表情符号", + "hide_custom_emoji": "隐藏自定义表情符号" }, "about": { "mrf": { @@ -1157,7 +1250,7 @@ "chats": "聊天", "delete": "删除", "message_user": "发消息给 {nickname}", - "you": "你:" + "you": "您:" }, "announcements": { "page_header": "公告", @@ -1198,8 +1291,8 @@ "update_changelog": "关于变化的更多细节,请参见 {theFullChangelog} 。", "update_changelog_here": "完整的更新日志", "big_update_title": "请忍耐一下", - "big_update_content": "我们已经有一段时间没有发布发行版,所以事情的外观和感觉可能与你习惯的不一样。", - "update_bugs": "请在 {pleromaGitlab} 上报告任何问题和bug,因为我们已经改变了很多,虽然我们进行了彻底的测试,并且自己使用了开发版本,但我们可能错过了一些东西。我们欢迎你对你可能遇到的问题或如何改进Pleroma和Pleroma-FE提出反馈和建议。", + "big_update_content": "我们已经有一段时间没有发布发行版,所以事情的外观和感觉可能与您习惯的不一样。", + "update_bugs": "请在 {pleromaGitlab} 上报告任何问题和 bug,因为我们改变了软件中的很多东西,虽然我们进行了彻底的测试,并且我们自己使用开发版本,但我们可能错过了一些东西。我们欢迎您对您可能遇到的问题或如何改进 Pleroma 和 Pleroma-FE 提出反馈和建议。", "art_by": "{linkToArtist} 的作品" }, "lists": { @@ -1232,13 +1325,14 @@ "nodb": "无数据库配置", "instance": "实例", "limits": "限制", - "frontends": "前端" + "frontends": "前端", + "emoji": "表情符号" }, "nodb": { "heading": "数据库配置已禁用", "documentation": "文档", "text2": "大多数配置选项将不可用。", - "text": "你需要修改后端配置文件,以便将 {property} 设置为 {value},更多内容请参见 {documentation}。" + "text": "您需要修改后端配置文件,以便将 {property} 设置为 {value},更多内容请参见 {documentation}。" }, "captcha": { "native": "本地", @@ -1281,8 +1375,11 @@ "set_default_version": "将版本 {version} 设为默认", "wip_notice": "请注意,此部分是一个WIP,缺乏某些功能,因为前端管理的后台实现并不完整。", "default_frontend": "默认前端", - "default_frontend_tip": "默认的前端将显示给所有用户。目前还没有办法让用户选择个人的前端。如果你不使用 PleromaFE,你很可能不得不使用旧的和有问题的 AdminFE 来进行实例配置,直到我们替换它。", - "available_frontends": "可供安装" + "default_frontend_tip": "默认的前端将显示给所有用户。目前还没有办法让用户选择自己的前端。如果您不使用 PleromaFE,您很可能不得不使用旧的和有问题的 AdminFE 来进行实例配置,直到我们替换它。", + "available_frontends": "可供安装", + "failure_installing_frontend": "无法安装前端 {version}:{reason}", + "success_installing_frontend": "前端 {version} 成功安装", + "default_frontend_unavail": "默认前端设置不可以,因为这需要数据库中的配置" }, "temp_overrides": { ":pleroma": { @@ -1306,6 +1403,50 @@ } } }, - "wip_notice": "此管理仪表板是实验性和 WIP 的,{adminFeLink}。" + "wip_notice": "此管理仪表板是实验性和 WIP 的,{adminFeLink}。", + "emoji": { + "remote_pack_instance": "远程表情包实例", + "fallback_src": "回退源", + "fallback_sha256": "回退 SHA256", + "delete_confirm": "您确定要删除 {0} 吗?", + "download_pack": "下载表情包", + "files": "文件", + "downloading_pack": "正在下载 {0}", + "download": "下载", + "download_as_name": "新名称", + "download_as_name_full": "新名称,留空来使用旧的名称", + "emoji_changed": "未保存的表情符号文件更改,检查突出显示的的表情符号", + "replace_warning": "这将替换本地同名的表情包", + "reload": "重新加载表情符号", + "create_pack": "创建表情包", + "emoji_pack": "表情包", + "save_meta": "保存元数据", + "delete": "删除", + "revert": "恢复", + "add_file": "添加文件", + "adding_new": "添加新的表情符号", + "shortcode": "简码", + "filename": "文件名", + "new_shortcode": "简码,留空来自动推断", + "emoji_packs": "表情包", + "remote_packs": "远程表情包", + "do_list": "列表", + "edit_pack": "编辑表情包", + "description": "描述", + "global_actions": "全局动作", + "importFS": "从文件系统导入表情符号", + "error": "错误:{0}", + "delete_pack": "删除表情包", + "new_pack_name": "新的表情包名称", + "create": "创建", + "homepage": "主页", + "share": "分享", + "save": "保存", + "revert_meta": "回复元数据", + "new_filename": "文件名,留空来自动推断", + "editing": "正在编辑 {0}", + "delete_title": "确定删除?", + "metadata_changed": "元数据和保存的不同" + } } } 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 24ed3cdb..9a96cf01 100644 --- a/src/services/locale/locale.service.js +++ b/src/services/locale/locale.service.js @@ -3,6 +3,7 @@ import ISO6391 from 'iso-639-1' import _ from 'lodash' const specialLanguageCodes = { + pdc: 'en', ja_easy: 'ja', zh_Hant: 'zh-HANT', zh: 'zh-Hans' @@ -18,6 +19,7 @@ const internalToBackendLocaleMulti = codes => { const getLanguageName = (code) => { const specialLanguageNames = { + pdc: 'Pennsilfaanisch-Deitsch', ja_easy: 'やさしいにほんご', 'nan-TW': '臺語(閩南語)', zh: '简体中文', diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js index 3c8e64bd..a4b7cbf0 100644 --- a/src/services/new_api/oauth.js +++ b/src/services/new_api/oauth.js @@ -10,7 +10,8 @@ export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) => const url = `${instance}/api/v1/apps` const form = new window.FormData() - form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`) + form.append('client_name', 'PleromaFE') + form.append('website', 'https://pleroma.social') form.append('redirect_uris', REDIRECT_URI) form.append('scopes', 'read write follow push admin') diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js new file mode 100644 index 00000000..5d71f35f --- /dev/null +++ b/src/services/theme_data/iss_deserializer.js @@ -0,0 +1,153 @@ +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') { + 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..959852b7 --- /dev/null +++ b/src/services/theme_data/iss_serializer.js @@ -0,0 +1,44 @@ +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': + return ` ${directive}: ${value.map(serializeShadow).join(', ')}` + default: + return ` ${directive}: ${value}` + } + } + }) + + return `${header} {\n${content.join(';\n')}\n}` + }).filter(x => x).join('\n\n') +} diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js index 95eb03c1..bcc0c961 100644 --- a/src/services/theme_data/theme2_to_theme3.js +++ b/src/services/theme_data/theme2_to_theme3.js @@ -418,7 +418,6 @@ export const convertTheme2To3 = (data) => { case 'Border': newRule.parent = rule newRule.directives.textColor = data.colors[key] - newRule.directives.textAuto = 'no-auto' variantArray = parts.slice(0, -1) break default: diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index cf58da11..39c8b74f 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -504,9 +504,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 } |
