logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 6bc020c733047d7033e508a2b4dffc581d703170
parent 83acbf953a4f50a017e3e857ecbd0b008f0b3be0
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date:   Wed, 31 Jul 2024 16:31:06 +0000

Merge branch 'release/2.7.x' into 'master'

Release 2.7.0

See merge request pleroma/pleroma-fe!1928

Diffstat:

M.gitignore1+
M.gitlab-ci.yml4++++
MCHANGELOG.md55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mindex.html2++
Mpackage.json23++++++++++++-----------
Apreview.style.js0
Msrc/App.scss464++++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/App.vue1+
Dsrc/_variables.scss36------------------------------------
Msrc/boot/after_store.js44++++++++++++++------------------------------
Msrc/boot/routes.js2++
Msrc/components/account_actions/account_actions.vue21+++++++--------------
Asrc/components/alert.style.js51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/announcement/announcement.vue8+++-----
Msrc/components/announcement_editor/announcement_editor.vue4+++-
Msrc/components/announcements_page/announcements_page.vue6++----
Msrc/components/attachment/attachment.scss31+++++++++++--------------------
Asrc/components/attachment/attachment.style.js24++++++++++++++++++++++++
Msrc/components/attachment/attachment.vue5++---
Msrc/components/autosuggest/autosuggest.vue18+++++++-----------
Msrc/components/avatar_list/avatar_list.vue5+----
Asrc/components/badge.style.js30++++++++++++++++++++++++++++++
Msrc/components/basic_user_card/basic_user_card.vue1-
Asrc/components/border.style.js13+++++++++++++
Asrc/components/button.style.js101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/button_unstyled.style.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/chat/chat.scss16+++-------------
Asrc/components/chat/chat.style.js19+++++++++++++++++++
Msrc/components/chat/chat.vue5++---
Msrc/components/chat_list/chat_list.vue5+----
Msrc/components/chat_list_item/chat_list_item.scss21++++-----------------
Msrc/components/chat_list_item/chat_list_item.vue3+--
Msrc/components/chat_message/chat_message.scss46++++++----------------------------------------
Asrc/components/chat_message/chat_message.style.js30++++++++++++++++++++++++++++++
Msrc/components/chat_message/chat_message.vue2+-
Msrc/components/chat_new/chat_new.scss5-----
Msrc/components/chat_new/chat_new.vue43++++++++++++++++++++++---------------------
Msrc/components/chat_title/chat_title.vue5+----
Msrc/components/checkbox/checkbox.vue29++++++++++++++---------------
Msrc/components/color_input/color_input.scss36++++++++++++++++++++++++++----------
Msrc/components/color_input/color_input.vue50+++++++++++++++++++++++++++++++-------------------
Msrc/components/conversation/conversation.js7++++++-
Msrc/components/conversation/conversation.vue91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/components/desktop_nav/desktop_nav.scss32++++----------------------------
Msrc/components/dialog_modal/dialog_modal.vue13++-----------
Msrc/components/emoji_input/emoji_input.vue57++++++++++++++++++++++++++-------------------------------
Msrc/components/emoji_picker/emoji_picker.js38+++++++++++++++++++++++++++++++-------
Msrc/components/emoji_picker/emoji_picker.scss103++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/components/emoji_picker/emoji_picker.vue21++++++++++++++++-----
Msrc/components/emoji_reactions/emoji_reactions.vue19++++++-------------
Msrc/components/extra_buttons/extra_buttons.vue30++++++++++++++----------------
Asrc/components/extra_notifications/extra_notifications.js48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/extra_notifications/extra_notifications.vue110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/favorite_button/favorite_button.vue4+---
Msrc/components/flash/flash.vue2--
Msrc/components/font_control/font_control.js70+++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/font_control/font_control.vue168++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Asrc/components/fun_text.style.js40++++++++++++++++++++++++++++++++++++++++
Msrc/components/gallery/gallery.vue2--
Msrc/components/global_notice_list/global_notice_list.vue44+-------------------------------------------
Asrc/components/icon.style.js14++++++++++++++
Msrc/components/image_cropper/image_cropper.vue2+-
Msrc/components/importer/importer.vue1+
Asrc/components/input.style.js60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/interactions/interactions.js1+
Msrc/components/interactions/interactions.vue7++++++-
Msrc/components/interface_language_switcher/interface_language_switcher.vue2--
Msrc/components/link-preview/link-preview.vue14++++----------
Asrc/components/link.style.js24++++++++++++++++++++++++
Msrc/components/list/list.vue26+++++++++-----------------
Asrc/components/list/list_item.style.js48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/lists_card/lists_card.vue16+---------------
Msrc/components/lists_edit/lists_edit.vue3+--
Msrc/components/lists_user_search/lists_user_search.vue3+--
Msrc/components/login_form/login_form.vue6++----
Msrc/components/media_upload/media_upload.vue2--
Msrc/components/mention_link/mention_link.scss13++++++-------
Msrc/components/mention_link/mention_link.vue2+-
Msrc/components/mentions_line/mentions_line.vue4++--
Asrc/components/menu_item.style.js90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/mfa_form/recovery_form.vue2+-
Msrc/components/mfa_form/totp_form.vue2+-
Asrc/components/mobile_drawer.style.js41+++++++++++++++++++++++++++++++++++++++++
Msrc/components/mobile_nav/mobile_nav.js23++++++++++++++++++-----
Msrc/components/mobile_nav/mobile_nav.vue66++++++++++++++++++++++++++++++------------------------------------
Msrc/components/mobile_post_status_button/mobile_post_status_button.vue7+------
Asrc/components/modal/modals.style.js9+++++++++
Msrc/components/moderation_tools/moderation_tools.vue38++++++++++++++++++--------------------
Msrc/components/mrf_transparency_panel/mrf_transparency_panel.vue1-
Msrc/components/nav_panel/nav_panel.vue50++++++--------------------------------------------
Msrc/components/navigation/navigation_entry.vue79+++++++++++++++++++++++++++++--------------------------------------------------
Msrc/components/navigation/navigation_pins.vue28+++++-----------------------
Msrc/components/notification/notification.js6++++++
Msrc/components/notification/notification.scss26+++++++++++---------------
Asrc/components/notification/notification.style.js18++++++++++++++++++
Msrc/components/notification/notification.vue8++++----
Msrc/components/notifications/notification_filters.vue37+++++++++++++++++++++++--------------
Msrc/components/notifications/notifications.js60++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/components/notifications/notifications.scss28+++++++++-------------------
Msrc/components/notifications/notifications.vue23+++++++++++++++++------
Msrc/components/opacity_input/opacity_input.vue2+-
Asrc/components/panel.style.js51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/panel_header.style.js24++++++++++++++++++++++++
Msrc/components/panel_loading/panel_loading.vue8++------
Msrc/components/password_reset/password_reset.vue11++---------
Msrc/components/poll/poll.js2+-
Msrc/components/poll/poll.vue30++++++++++++++++--------------
Msrc/components/poll/poll_form.vue6++----
Asrc/components/poll/poll_graph.style.js12++++++++++++
Asrc/components/popover.style.js36++++++++++++++++++++++++++++++++++++
Msrc/components/popover/popover.vue124++++++++++++++++---------------------------------------------------------------
Msrc/components/post_status_form/post_status_form.js5+++--
Msrc/components/post_status_form/post_status_form.vue67++++++++++++++++---------------------------------------------------
Msrc/components/quick_filter_settings/quick_filter_settings.js7+++++++
Msrc/components/quick_filter_settings/quick_filter_settings.vue38+++++++++++++++++++++++++-------------
Msrc/components/quick_view_settings/quick_view_settings.js8+++++++-
Msrc/components/quick_view_settings/quick_view_settings.vue22+++++++++++-----------
Asrc/components/quotes_timeline/quotes_timeline.js26++++++++++++++++++++++++++
Asrc/components/quotes_timeline/quotes_timeline.vue10++++++++++
Msrc/components/range_input/range_input.vue6+++---
Msrc/components/react_button/react_button.vue8+-------
Msrc/components/registration/registration.js10++++++++--
Msrc/components/registration/registration.vue40++++++++++++++++++++++++----------------
Msrc/components/reply_button/reply_button.vue4+---
Msrc/components/report/report.js1-
Msrc/components/report/report.scss10++--------
Msrc/components/report/report.vue2+-
Msrc/components/retweet_button/retweet_button.vue4+---
Msrc/components/rich_content/rich_content.jsx8+++++++-
Msrc/components/rich_content/rich_content.scss30++++++++++++++++++++++++++----
Asrc/components/rich_content/rich_content.style.js18++++++++++++++++++
Asrc/components/root.style.js49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/scope_selector/scope_selector.js8++++----
Msrc/components/scope_selector/scope_selector.vue7-------
Msrc/components/screen_reader_notice/screen_reader_notice.js2+-
Asrc/components/scrollbar.style.js11+++++++++++
Asrc/components/scrollbar_element.style.js101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/search/search.vue34++++++++++------------------------
Msrc/components/search_bar/search_bar.vue7++-----
Msrc/components/select/select.vue11+++--------
Msrc/components/selectable_list/selectable_list.vue29+++++++++++------------------
Asrc/components/settings_modal/admin_tabs/emoji_tab.js257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/admin_tabs/emoji_tab.scss59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/admin_tabs/emoji_tab.vue358+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/admin_tabs/frontends_tab.js10+++++++---
Msrc/components/settings_modal/admin_tabs/frontends_tab.vue29+++++++++++++++++++++--------
Msrc/components/settings_modal/admin_tabs/instance_tab.vue10++++++++--
Msrc/components/settings_modal/helpers/attachment_setting.vue2+-
Asrc/components/settings_modal/helpers/emoji_editing_popover.vue227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/helpers/modified_indicator.vue10++++++++--
Msrc/components/settings_modal/helpers/number_setting.vue3++-
Msrc/components/settings_modal/helpers/setting.js13+++++++++++--
Dsrc/components/settings_modal/helpers/size_setting.js40----------------------------------------
Dsrc/components/settings_modal/helpers/size_setting.vue62--------------------------------------------------------------
Msrc/components/settings_modal/helpers/string_setting.vue2+-
Asrc/components/settings_modal/helpers/unit_setting.js64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/unit_setting.vue62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/settings_modal.js3+++
Msrc/components/settings_modal/settings_modal.scss14++++++++++++--
Msrc/components/settings_modal/settings_modal.vue20++++++++++++++++----
Msrc/components/settings_modal/settings_modal_admin_content.js4+++-
Msrc/components/settings_modal/settings_modal_admin_content.scss11++---------
Msrc/components/settings_modal/settings_modal_admin_content.vue8++++++++
Msrc/components/settings_modal/settings_modal_user_content.js8++++++--
Msrc/components/settings_modal/settings_modal_user_content.scss11++---------
Msrc/components/settings_modal/settings_modal_user_content.vue47+++++++++++++++++++++++++++--------------------
Asrc/components/settings_modal/tabs/appearance_tab.js195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/appearance_tab.vue313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/tabs/filtering_tab.js2++
Msrc/components/settings_modal/tabs/filtering_tab.vue25+++++++++++++++++++++++--
Msrc/components/settings_modal/tabs/general_tab.js29++---------------------------
Msrc/components/settings_modal/tabs/general_tab.vue75---------------------------------------------------------------------------
Msrc/components/settings_modal/tabs/notifications_tab.js4++++
Msrc/components/settings_modal/tabs/notifications_tab.vue242++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/components/settings_modal/tabs/profile_tab.js13+++++++++++--
Msrc/components/settings_modal/tabs/profile_tab.scss8++------
Msrc/components/settings_modal/tabs/profile_tab.vue30++++++++++++++++++++++++------
Msrc/components/settings_modal/tabs/security_tab/mfa.vue7+++----
Msrc/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue7++-----
Msrc/components/settings_modal/tabs/security_tab/mfa_totp.vue1+
Msrc/components/settings_modal/tabs/security_tab/security_tab.vue9+++++++++
Dsrc/components/settings_modal/tabs/theme_tab/preview.vue149-------------------------------------------------------------------------------
Asrc/components/settings_modal/tabs/theme_tab/theme_preview.vue250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js121++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.scss122+++++++------------------------------------------------------------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.vue203++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/shadow_control/shadow_control.js2+-
Msrc/components/shadow_control/shadow_control.vue34+++++++++++++---------------------
Msrc/components/shout_panel/shout_panel.vue16++++++----------
Msrc/components/side_drawer/side_drawer.vue96+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Asrc/components/status/post.style.js42++++++++++++++++++++++++++++++++++++++++++
Msrc/components/status/status.js58+++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/components/status/status.scss57+++++++++++++++++----------------------------------------
Msrc/components/status/status.vue75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/components/status_body/status_body.scss14+-------------
Msrc/components/status_body/status_body.vue2++
Msrc/components/status_popover/status_popover.vue7+------
Msrc/components/sticker_picker/sticker_picker.vue4+---
Msrc/components/still-image/still-image.vue5+----
Asrc/components/tab_switcher/tab.style.js78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/tab_switcher/tab_switcher.jsx2+-
Msrc/components/tab_switcher/tab_switcher.scss30+++++++++++++++---------------
Asrc/components/text.style.js22++++++++++++++++++++++
Msrc/components/thread_tree/thread_tree.vue8+++-----
Msrc/components/timeline/timeline.js11+++++++----
Msrc/components/timeline/timeline.scss26++++++++++----------------
Msrc/components/timeline/timeline.vue2+-
Msrc/components/timeline_menu/timeline_menu.js3++-
Msrc/components/timeline_menu/timeline_menu.vue63---------------------------------------------------------------
Asrc/components/top_bar.style.js28++++++++++++++++++++++++++++
Asrc/components/underlay.style.js19+++++++++++++++++++
Msrc/components/update_notification/update_notification.scss4+---
Asrc/components/user_avatar/avatar.style.js22++++++++++++++++++++++
Msrc/components/user_avatar/user_avatar.js8+++++---
Msrc/components/user_avatar/user_avatar.vue33++++++++++++++++-----------------
Msrc/components/user_card/user_card.js2+-
Msrc/components/user_card/user_card.scss72+++++++++++++++++++-----------------------------------------------------
Asrc/components/user_card/user_card.style.js41+++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_card/user_card.vue23+++++++++++++----------
Msrc/components/user_list_menu/user_list_menu.vue6+++---
Msrc/components/user_list_popover/user_list_popover.vue2--
Msrc/components/user_note/user_note.vue6++----
Msrc/components/user_panel/user_panel.vue13++++++++++---
Msrc/components/user_popover/user_popover.vue2--
Msrc/components/user_profile/user_profile.js5+++++
Msrc/components/user_profile/user_profile.vue115+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/components/user_reporting_modal/user_reporting_modal.vue10+++-------
Msrc/components/video_attachment/video_attachment.vue2+-
Msrc/hocs/with_load_more/with_load_more.scss5+----
Msrc/i18n/cs.json778++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/i18n/en.json169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/i18n/eo.json133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/i18n/fr.json314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/i18n/ja_easy.json30++++++++++++++++++++++--------
Msrc/i18n/ja_pedantic.json920++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/i18n/ko.json159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/i18n/nan-TW.json606+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/i18n/pt.json185++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/i18n/uk.json398++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/i18n/zh.json28++++++++++++++++++++++------
Msrc/lib/persisted_state.js8++++----
Msrc/main.js2++
Msrc/modules/adminSettings.js2--
Msrc/modules/api.js5+++--
Msrc/modules/config.js133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/modules/instance.js43++++++++++++++++++-------------------------
Msrc/modules/interface.js235++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/modules/notifications.js169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/modules/serverSideStorage.js1-
Msrc/modules/statuses.js178++++++++-----------------------------------------------------------------------
Msrc/modules/users.js27+++++++++++++++++++++------
Msrc/panel.scss118+++++++++++++++++++++++++++++--------------------------------------------------
Msrc/services/api/api.service.js144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/services/backend_interactor_service/backend_interactor_service.js4++--
Msrc/services/color_convert/color_convert.js2+-
Msrc/services/desktop_notification_utils/desktop_notification_utils.js39++++++++++++++++++++++++++++++++++-----
Msrc/services/entity_normalizer/entity_normalizer.service.js4+++-
Msrc/services/favicon_service/favicon_service.js5++++-
Msrc/services/file_type/file_type.service.js2+-
Msrc/services/notification_utils/notification_utils.js91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/services/notifications_fetcher/notifications_fetcher.service.js18+++++++++++++-----
Dsrc/services/push/push.js111-------------------------------------------------------------------------------
Msrc/services/style_setter/style_setter.js580++++++++++++++++++++++++++++++-------------------------------------------------
Asrc/services/sw/sw.js148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/css_utils.js173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/iss_utils.js168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/pleromafe.t3.js2++
Asrc/services/theme_data/theme2_keys.js177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/theme2_to_theme3.js539+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/theme3_slot_functions.js103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/services/theme_data/theme_data.service.js347++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/services/theme_data/theme_data_3.service.js513+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/services/timeline_fetcher/timeline_fetcher.service.js8+++++---
Msrc/sw.js100++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtest/e2e/nightwatch.conf.js2+-
Mtest/unit/specs/modules/statuses.spec.js76----------------------------------------------------------------------------
Mtest/unit/specs/services/notification_utils/notification_utils.spec.js78+++++++++++++++++++++++++++++++++++++++---------------------------------------
Atest/unit/specs/services/theme_data/theme_data3.spec.js150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/check-changelog2+-
Myarn.lock626+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
280 files changed, 12906 insertions(+), 4200 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -8,3 +8,4 @@ selenium-debug.log .idea/ config/local.json static/emoji.json +logs/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml @@ -43,6 +43,8 @@ lint: test: stage: test + tags: + - amd64 variables: APT_CACHE_DIR: apt-cache script: @@ -54,6 +56,8 @@ test: build: stage: build + tags: + - amd64 script: - yarn - npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -3,6 +3,61 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2.7.0 + +### Known issues +We got some reports related to emoji picker performance, this hopefully will be fixed in 2.7.1. + +### Notes +This release overhauls how themes work, themes now need to be "compiled", which can cause some delay when loading for the first time and temporarily look "wrong" in some places (popups, menus, dialogs). Please do report any issues, especially if your theme looks wrong or breaks interface when loading. Also report issues if you're experiencing constant performance issues. + +To admins: remember that you can update PleromaFE to recent `master` or `develop` in admin dashboard in "Front-ends" tab, scroll down to find PleromaFE box and click "Reinstall `master`" or dropdown and then "Reinstall `develop`". Currently there is no mechanism to check if there is an update or not. + +### Changed +- Overhauled the way themes work, migrating to new Pleroma Interface Style Sheets system aka "Themes 3". +- Notifications are no longer sorted by "seen" status since interacting with them can change their read status and makes UI jumpy. Old behavior can be restored in settings. +- Notifications are now shown through a ServiceWorker (since mobile chrome does not allow them otherwise), it's always enabled, even if previously we only enabled it for WebPush notifications only. If you don't like websites "running" while closed, check how to disable them in your browser. Old way to show notifications will be used as a fallback but might not have all the new features. +- Reorganized Settings modal to move out visual stuff into Appearance tab + +### Added +- Emoji pack management to the admin panel +- Support `status` notification type (subscriptions/bell, fixes PleromaFE on newer PleromaBE versions) +- Poll end notifications. +- Added option to not mark all notifications when closing notifications drawer on mobile, this creates a new button to mark all as seen. +- Option to always "show" notifications when using web push for better compatibility with some browsers (chrome, edge, safari) +- Option to toggle what notification types appear in native notifications, by default less important ones (likes, repeats, etc) will no longer show up in native notifications. +- Option to treat non-interactive notifications (likes, repeats et all) as seen for visual purposes (no read mark, ignored in counters, still can show in native notifications) +- Ability to resize UI (and certain components) scale independent of browser/text scale +- Ability to override certain aspects of UI style independent of theme used (UI roundness, fonts, underlay) +- Theme selector with visual previews of the theme +- Display loading and error indicator for conversation page +- Option to only show scrobbles that are recent enough +- Interacting (opening reply box etc) or simply clicking on non-interactive notifications now marks them as read. Clicking on native notifications for non-interactive ones also marks them as seen. +- Support group actors +- Focusing into a tab clears all current desktop notifications +- Ability to change size of emoji +- Ability to view APNG (Animated PNG) attachments. +- Support showing extra notifications in the notifications column +- Create a link to the URL of the scrobble when it's present +- Allow hiding custom emojis in picker. +- Ability to mute sensitive posts (ported from eintei). +- Native notifications now also have "badge" property that matches instance's favicon (visible in Android Chromium at least) +- Display public favorites on user profiles +- Display quotes count on posts and add quotes list page +- Show a dedicated registration notice page when further action is required after registering + +### Fixed +- Synchronized requested notification types with backend, hopefully should fix missing notifications for polls and follow requests +- Error that appeared on mobile Chromium (and derivatives) when native notifications are allowed +- Being unable to set notification visibility for reports and follow requests +- Native notifications appearing as many times as there are open tabs. Clicking on notification will focus last focused tab. +- The expiry date indication won't be shown if the poll never expires +- Profile mentions causing a 422 error on newer PleromaBE versions. +- Color inputs are less ugly now +- Unread notifications should now properly catch up between sessions (eventually) in polling mode +- Video posters on Safari + + ## 2.6.1 ### Fixed - fix admin dashboard not having any feedback on frontend installation diff --git a/index.html b/index.html @@ -4,6 +4,8 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <link rel="icon" type="image/png" href="/favicon.png"> + <style id="pleroma-eager-styles" type="text/css"></style> + <style id="pleroma-lazy-styles" type="text/css"></style> <!--server-generated-meta--> </head> <body class="hidden"> diff --git a/package.json b/package.json @@ -1,6 +1,6 @@ { "name": "pleroma_fe", - "version": "2.6.1", + "version": "2.7.0", "description": "Pleroma frontend, the default frontend of Pleroma social network server", "author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>", "private": false, @@ -24,15 +24,16 @@ "@fortawesome/vue-fontawesome": "3.0.3", "@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@kazvmoe-infra/unicode-emoji-json": "0.4.0", - "@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12", - "@vuelidate/core": "2.0.2", - "@vuelidate/validators": "2.0.0", + "@ruffle-rs/ruffle": "0.1.0-nightly.2024.3.17", + "@vuelidate/core": "2.0.3", + "@vuelidate/validators": "2.0.4", "body-scroll-lock": "3.1.5", "chromatism": "3.0.0", "click-outside-vue3": "4.0.1", "cropperjs": "1.5.13", "escape-html": "1.0.3", - "js-cookie": "3.0.1", + "hash-sum": "^2.0.0", + "js-cookie": "3.0.5", "localforage": "1.10.0", "parse-link-header": "2.0.0", "phoenix": "1.7.7", @@ -55,13 +56,13 @@ "@babel/preset-env": "7.21.5", "@babel/register": "7.21.0", "@intlify/vue-i18n-loader": "5.0.1", - "@ungap/event-target": "0.2.3", + "@ungap/event-target": "0.2.4", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", - "@vue/babel-plugin-jsx": "1.1.1", + "@vue/babel-plugin-jsx": "1.2.1", "@vue/compiler-sfc": "3.2.45", "@vue/test-utils": "2.2.8", - "autoprefixer": "10.4.14", - "babel-loader": "9.1.2", + "autoprefixer": "10.4.19", + "babel-loader": "9.1.3", "babel-plugin-lodash": "3.3.4", "chai": "4.3.7", "chalk": "1.1.3", @@ -69,7 +70,7 @@ "connect-history-api-fallback": "2.0.0", "copy-webpack-plugin": "11.0.0", "cross-spawn": "7.0.3", - "css-loader": "6.7.3", + "css-loader": "6.10.0", "css-minimizer-webpack-plugin": "4.2.2", "custom-event-polyfill": "1.0.7", "eslint": "8.33.0", @@ -99,7 +100,7 @@ "lodash": "4.17.21", "mini-css-extract-plugin": "2.7.6", "mocha": "10.2.0", - "nightwatch": "2.6.20", + "nightwatch": "2.6.25", "opn": "5.5.0", "ora": "0.4.1", "postcss": "8.4.23", diff --git a/preview.style.js b/preview.style.js diff --git a/src/App.scss b/src/App.scss @@ -1,10 +1,9 @@ // stylelint-disable rscss/class-format /* stylelint-disable no-descending-specificity */ -@import "./variables"; @import "./panel"; :root { - --navbar-height: 3.5rem; + --status-margin: 0.75em; --post-line-height: 1.4; // Z-Index stuff --ZI_media_modal: 9000; @@ -13,19 +12,25 @@ --ZI_navbar_popovers: 7500; --ZI_navbar: 7000; --ZI_popovers: 6000; + + // Fallback for when stuff is loading + --background: var(--bg); } html { - font-size: 14px; + font-size: var(--textSize, 14px); + + --navbar-height: var(--navbarSize, 3.5rem); + --emoji-size: var(--emojiSize, 32px); + --panel-header-height: var(--panelHeaderSize, 3.2rem); // overflow-x: clip causes my browser's tab to crash with SIGILL lul } body { font-family: sans-serif; - font-family: var(--interfaceFont, sans-serif); + font-family: var(--font); margin: 0; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overscroll-behavior-y: none; @@ -42,17 +47,35 @@ body { // have a cursor/pointer to operate them @media (any-pointer: fine) { * { - scrollbar-color: var(--btn) transparent; + scrollbar-color: var(--fg) transparent; &::-webkit-scrollbar { background: transparent; } + &::-webkit-scrollbar-corner { + background: transparent; + } + + &::-webkit-resizer { + /* stylelint-disable-next-line declaration-no-important */ + background-color: transparent !important; + background-image: + linear-gradient( + 135deg, + transparent calc(50% - 1px), + var(--textFaint) 50%, + transparent calc(50% + 1px), + transparent calc(75% - 1px), + var(--textFaint) 75%, + transparent calc(75% + 1px), + ); + } + &::-webkit-scrollbar-button, &::-webkit-scrollbar-thumb { - background-color: var(--btn); - box-shadow: var(--buttonShadow); - border-radius: var(--btnRadius); + box-shadow: var(--shadow); + border-radius: var(--roundness); } // horizontal/vertical/increment/decrement are webkit-specific stuff @@ -61,7 +84,7 @@ body { &::-webkit-scrollbar-button { --___bgPadding: 2px; - color: var(--btnText); + color: var(--text); background-repeat: no-repeat, no-repeat; &:horizontal { @@ -69,15 +92,15 @@ body { &:increment { background-image: - linear-gradient(45deg, var(--btnText) 50%, transparent 51%), - linear-gradient(-45deg, transparent 50%, var(--btnText) 51%); + linear-gradient(45deg, var(--text) 50%, transparent 51%), + linear-gradient(-45deg, transparent 50%, var(--text) 51%); background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding); } &:decrement { background-image: - linear-gradient(45deg, transparent 50%, var(--btnText) 51%), - linear-gradient(-45deg, var(--btnText) 50%, transparent 51%); + linear-gradient(45deg, transparent 50%, var(--text) calc(50% + 1px)), + linear-gradient(-45deg, var(--text) 50%, transparent 51%); background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding); } } @@ -87,15 +110,15 @@ body { &:increment { background-image: - linear-gradient(-45deg, transparent 50%, var(--btnText) 51%), - linear-gradient(45deg, transparent 50%, var(--btnText) 51%); + linear-gradient(-45deg, transparent 50%, var(--text) 51%), + linear-gradient(45deg, transparent 50%, var(--text) 51%); background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%; } &:decrement { background-image: - linear-gradient(-45deg, var(--btnText) 50%, transparent 51%), - linear-gradient(45deg, var(--btnText) 50%, transparent 51%); + linear-gradient(-45deg, var(--text) 50%, transparent 51%), + linear-gradient(45deg, var(--text) 50%, transparent 51%); background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%; } } @@ -104,15 +127,14 @@ body { } // Body should have background to scrollbar otherwise it will use white (body color?) html { - scrollbar-color: var(--selectedMenu) var(--wallpaper); + scrollbar-color: var(--fg) var(--wallpaper); background: var(--wallpaper); } } a { text-decoration: none; - color: $fallback--link; - color: var(--link, $fallback--link); + color: var(--link); } h4 { @@ -128,29 +150,15 @@ h4 { i[class*="icon-"], .svg-inline--fa, .iconLetter { - color: $fallback--icon; - color: var(--icon, $fallback--icon); -} - -.button-unstyled:hover, -a:hover { - > i[class*="icon-"], - > .svg-inline--fa, - > .iconLetter { - color: var(--text); - } + color: var(--icon); } nav { z-index: var(--ZI_navbar); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - color: $fallback--faint; - color: var(--faint, $fallback--faint); - box-shadow: 0 0 4px rgb(0 0 0 / 60%); - box-shadow: var(--topBarShadow); + box-shadow: var(--shadow); box-sizing: border-box; height: var(--navbar-height); + font-size: calc(var(--navbar-height) / 3.5); position: fixed; } @@ -195,16 +203,14 @@ nav { grid-column: 1 / span 3; grid-row: 1 / 1; pointer-events: none; - background-color: rgb(0 0 0 / 15%); - background-color: var(--underlay, rgb(0 0 0 / 15%)); + background-color: var(--underlay); z-index: -1000; } .app-layout { --miniColumn: 25rem; --maxiColumn: 45rem; - --columnGap: 1em; - --status-margin: 0.75em; + --columnGap: 1rem; --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); @@ -366,106 +372,112 @@ nav { .button-default { user-select: none; - color: $fallback--text; - color: var(--btnText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); + color: var(--text); border: none; - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); cursor: pointer; - box-shadow: $fallback--buttonShadow; - box-shadow: var(--buttonShadow); + background-color: var(--background); + box-shadow: var(--shadow); font-size: 1em; font-family: sans-serif; - font-family: var(--interfaceFont, sans-serif); + font-family: var(--font); - &.-sublime { - background: transparent; + &::-moz-focus-inner { + border: none; } - i[class*="icon-"], - .svg-inline--fa { - color: $fallback--text; - color: var(--btnText, $fallback--text); + &:disabled { + cursor: not-allowed; } +} - &::-moz-focus-inner { - border: none; +.menu-item, +.list-item { + display: block; + box-sizing: border-box; + border: none; + outline: none; + text-align: initial; + font-size: inherit; + font-family: inherit; + font-weight: 400; + cursor: pointer; + color: inherit; + clear: both; + position: relative; + white-space: nowrap; + border-color: var(--border); + border-style: solid; + border-width: 0; + border-top-width: 1px; + width: 100%; + line-height: var(--__line-height); + padding: var(--__vertical-gap) var(--__horizontal-gap); + background: transparent; + + --__line-height: 1.5em; + --__horizontal-gap: 0.75em; + --__vertical-gap: 0.5em; + + &.-non-interactive { + cursor: auto; } + &.-active, &:hover { - box-shadow: 0 0 4px rgb(255 255 255 / 30%); - box-shadow: var(--buttonHoverShadow); - } - - &:active { - box-shadow: - 0 0 4px 0 rgb(255 255 255 / 30%), - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset; - box-shadow: var(--buttonPressedShadow); - color: $fallback--text; - color: var(--btnPressedText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnPressed, $fallback--fg); - - svg, - i { - color: $fallback--text; - color: var(--btnPressedText, $fallback--text); - } + border-top-width: 1px; + border-bottom-width: 1px; } - &:disabled { - cursor: not-allowed; - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnDisabled, $fallback--fg); - - svg, - i { - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - } + &.-active + &, + &:hover + & { + border-top-width: 0; } - &.toggled { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnToggled, $fallback--fg); - box-shadow: - 0 0 4px 0 rgb(255 255 255 / 30%), - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset; - box-shadow: var(--buttonPressedShadow); - - svg, - i { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - } + &:hover + .menu-item-collapsible:not(.-expanded) + &, + &.-active + .menu-item-collapsible:not(.-expanded) + & { + border-top-width: 0; } - &.danger { - // TODO: add better color variable - color: $fallback--text; - color: var(--alertErrorPanelText, $fallback--text); - background-color: $fallback--alertError; - background-color: var(--alertError, $fallback--alertError); + &[aria-expanded="true"] { + border-bottom-width: 1px; + } + + a, + button:not(.button-default) { + text-align: initial; + padding: 0; + background: none; + border: none; + outline: none; + display: inline; + font-size: 100%; + font-family: inherit; + line-height: unset; + color: var(--text); + } + + &:first-child { + border-top-right-radius: var(--roundness); + border-top-left-radius: var(--roundness); + border-top-width: 0; + } + + &:last-child { + border-bottom-right-radius: var(--roundness); + border-bottom-left-radius: var(--roundness); + border-bottom-width: 0; } } .button-unstyled { - background: none; border: none; outline: none; display: inline; text-align: initial; font-size: 100%; font-family: inherit; + box-shadow: var(--shadow); + background-color: transparent; padding: 0; line-height: unset; cursor: pointer; @@ -473,28 +485,23 @@ nav { color: inherit; &.-link { - color: $fallback--link; - color: var(--link, $fallback--link); - } - - &.-fullwidth { - width: 100%; - } - - &.-hover-highlight { - &:hover svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } + /* stylelint-disable-next-line declaration-no-important */ + color: var(--link) !important; } } input, -textarea, +textarea { + border: none; + display: inline-block; + outline: none; +} + .input { &.unstyled { border-radius: 0; - background: none; + /* stylelint-disable-next-line declaration-no-important */ + background: none !important; box-shadow: none; height: unset; } @@ -502,19 +509,10 @@ textarea, --_padding: 0.5em; border: none; - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); - box-shadow: - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset, - 0 0 2px 0 rgb(0 0 0 / 100%) inset; - box-shadow: var(--inputShadow); - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); + background-color: var(--background); + color: var(--text); + box-shadow: var(--shadow); + font-family: var(--font); font-size: 1em; margin: 0; box-sizing: border-box; @@ -528,7 +526,6 @@ textarea, &[disabled="disabled"], &.disabled { cursor: not-allowed; - opacity: 0.5; } &[type="range"] { @@ -543,9 +540,9 @@ textarea, display: none; &:checked + label::before { - box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset; - box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset; - background-color: var(--accent, $fallback--link); + box-shadow: var(--shadow); + background-color: var(--background); + color: var(--text); } &:disabled { @@ -559,16 +556,14 @@ textarea, + label::before { flex-shrink: 0; display: inline-block; - content: ""; + content: "•"; transition: box-shadow 200ms; width: 1.1em; height: 1.1em; border-radius: 100%; // Radio buttons should always be circle - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); + background-color: var(--background); + box-shadow: var(--shadow); margin-right: 0.5em; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1; @@ -581,8 +576,9 @@ textarea, &[type="checkbox"] { &:checked + label::before { - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); + background-color: var(--background); + box-shadow: var(--shadow); } &:disabled { @@ -600,13 +596,9 @@ textarea, transition: color 200ms; width: 1.1em; height: 1.1em; - border-radius: $fallback--checkboxRadius; - border-radius: var(--checkboxRadius, $fallback--checkboxRadius); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); + border-radius: var(--roundness); + box-shadow: var(--shadow); margin-right: 0.5em; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1; @@ -622,17 +614,26 @@ textarea, } } +.input, +.button-default { + --_roundness-left: var(--roundness); + --_roundness-right: var(--roundness); + + border-top-left-radius: var(--_roundness-left); + border-bottom-left-radius: var(--_roundness-left); + border-top-right-radius: var(--_roundness-right); + border-bottom-right-radius: var(--_roundness-right); +} + // Textareas should have stock line-height + vertical padding instead of huge line-height -textarea { +textarea.input { padding: var(--_padding); line-height: var(--post-line-height); } option { - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + color: var(--text); + background-color: var(--background); } .hide-number-spinner { @@ -653,7 +654,7 @@ option { li { border: 1px solid var(--border); - border-radius: var(--inputRadius); + border-radius: var(--roundness); padding: 0.5em; margin: 0.25em; } @@ -669,22 +670,23 @@ option { display: inline-flex; vertical-align: middle; - button, - .button-dropdown { + > *, + > * .button-default { + --_roundness-left: 0; + --_roundness-right: 0; + position: relative; flex: 1 1 auto; + } - &:not(:last-child), - &:not(:last-child) .button-default { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } + > *:first-child, + > *:first-child .button-default { + --_roundness-left: var(--roundness); + } - &:not(:first-child), - &:not(:first-child) .button-default { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } + > *:last-child, + > *:last-child .button-default { + --_roundness-right: var(--roundness); } } @@ -714,74 +716,58 @@ option { overflow: hidden; text-overflow: ellipsis; - &.badge-notification { - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - color: white; - color: var(--badgeNotificationText, white); - } -} - -.alert { - margin: 0 0.35em; - padding: 0 0.25em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - - &.error { - background-color: $fallback--alertError; - background-color: var(--alertError, $fallback--alertError); - color: $fallback--text; - color: var(--alertErrorText, $fallback--text); - - .panel-heading & { - color: $fallback--text; - color: var(--alertErrorPanelText, $fallback--text); - } + &.-dot, + &.-counter { + margin: 0; + position: absolute; } - &.warning { - background-color: $fallback--alertWarning; - background-color: var(--alertWarning, $fallback--alertWarning); - color: $fallback--text; - color: var(--alertWarningText, $fallback--text); - - .panel-heading & { - color: $fallback--text; - color: var(--alertWarningPanelText, $fallback--text); - } + &.-dot { + min-height: 8px; + max-height: 8px; + min-width: 8px; + max-width: 8px; + padding: 0; + line-height: 0; + font-size: 0; + left: calc(50% - 4px); + top: calc(50% - 4px); + margin-left: 6px; + margin-top: -6px; } - &.success { - background-color: var(--alertSuccess, $fallback--alertWarning); - color: var(--alertSuccessText, $fallback--text); - - .panel-heading & { - color: var(--alertSuccessPanelText, $fallback--text); - } + &.-counter { + border-radius: var(--roundness); + font-size: 0.75em; + line-height: 1; + text-align: right; + padding: 0.2em; + min-width: 0; + left: calc(50% - 0.5em); + top: calc(50% - 0.4em); + margin-left: 0.7em; + margin-top: -1em; } } -.faint { - color: $fallback--faint; - color: var(--faint, $fallback--faint); +.alert { + margin: 0 0.35em; + padding: 0 0.25em; + border-radius: var(--roundness); + border: 1px solid var(--border); } -.faint-link { - color: $fallback--faint; - color: var(--faint, $fallback--faint); +.faint { + --text: var(--textFaint); + --link: var(--linkFaint); - &:hover { - text-decoration: underline; - } + color: var(--text); } .visibility-notice { padding: 0.5em; - border: 1px solid $fallback--faint; - border: 1px solid var(--faint, $fallback--faint); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border: 1px solid var(--textFaint); + border-radius: var(--roundness); } .notice-dismissible { @@ -802,6 +788,10 @@ option { &.iconLetter { font-size: 1.1em; } + + &.svg-inline--fa { + vertical-align: -0.15em; + } } .fa-old-padding { @@ -816,6 +806,11 @@ option { opacity: 0.25; } +.timeago { + --link: var(--text); + --linkFaint: var(--textFaint); +} + .login-hint { text-align: center; @@ -914,3 +909,8 @@ option { padding: 0; position: absolute; } + +*::selection { + color: var(--selectionText); + background-color: var(--selectionBackground); +} diff --git a/src/App.vue b/src/App.vue @@ -1,5 +1,6 @@ <template> <div + v-show="$store.state.interface.themeApplied" id="app-loaded" :style="bgStyle" > diff --git a/src/_variables.scss b/src/_variables.scss @@ -1,36 +0,0 @@ -$main-color: #f58d2c; -$main-background: white; -$darkened-background: whitesmoke; - -$fallback--bg: #121a24; -$fallback--fg: #182230; -$fallback--faint: rgb(185 185 186 / 50%); -$fallback--text: #b9b9ba; -$fallback--link: #d8a070; -$fallback--icon: #666; -$fallback--lightBg: rgb(21 30 42); -$fallback--lightText: #b9b9ba; -$fallback--border: #222; -$fallback--cRed: #f00; -$fallback--cBlue: #0095ff; -$fallback--cGreen: #0fa00f; -$fallback--cOrange: orange; - -$fallback--alertError: rgb(211 16 20 / 50%); -$fallback--alertWarning: rgb(111 111 20 / 50%); - -$fallback--panelRadius: 10px; -$fallback--checkboxRadius: 2px; -$fallback--btnRadius: 4px; -$fallback--inputRadius: 4px; -$fallback--tooltipRadius: 5px; -$fallback--avatarRadius: 4px; -$fallback--avatarAltRadius: 10px; -$fallback--attachmentRadius: 10px; -$fallback--chatMessageRadius: 10px; - -$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%), - 0 1px 0 0 rgb(255 255 255 / 20%) inset, - 0 -1px 0 0 rgb(0 0 0 / 20%) inset; - -$status-margin: 0.75em; diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -13,9 +13,9 @@ import VBodyScrollLock from 'src/directives/body_scroll_lock' import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' -import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js' +import { applyConfig } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' +import { initServiceWorker, updateFocus } from '../services/sw/sw.js' let staticInitialResults = null @@ -159,8 +159,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('showFeaturesPanel') copyInstanceOption('hideSitename') copyInstanceOption('sidebarRight') - - return store.dispatch('setTheme', config.theme) } const getTOS = async ({ store }) => { @@ -260,6 +258,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') }) + store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') }) const uploadLimits = metadata.uploadLimits store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) @@ -326,17 +325,14 @@ const setConfig = async ({ store }) => { } const checkOAuthToken = async ({ store }) => { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - if (store.getters.getUserToken()) { - try { - await store.dispatch('loginUser', store.getters.getUserToken()) - } catch (e) { - console.error(e) - } + if (store.getters.getUserToken()) { + try { + await store.dispatch('loginUser', store.getters.getUserToken()) + } catch (e) { + console.error(e) } - resolve() - }) + } + return Promise.resolve() } const afterStoreSetup = async ({ store, i18n }) => { @@ -344,28 +340,16 @@ const afterStoreSetup = async ({ store, i18n }) => { store.dispatch('setLayoutHeight', windowHeight()) FaviconService.initFaviconService() + initServiceWorker(store) + + window.addEventListener('focus', () => updateFocus()) const overrides = window.___pleromafe_dev_overrides || {} const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin store.dispatch('setInstanceOption', { name: 'server', value: server }) await setConfig({ store }) - - const { customTheme, customThemeSource } = store.state.config - const { theme } = store.state.instance - const customThemePresent = customThemeSource || customTheme - - if (customThemePresent) { - if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { - applyTheme(customThemeSource) - } else { - applyTheme(customTheme) - } - } else if (theme) { - // do nothing, it will load asynchronously - } else { - console.error('Failed to load any theme!') - } + await store.dispatch('setTheme') applyConfig(store.state.config) diff --git a/src/boot/routes.js b/src/boot/routes.js @@ -25,6 +25,7 @@ import ListsTimeline from 'components/lists_timeline/lists_timeline.vue' import ListsEdit from 'components/lists_edit/lists_edit.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' +import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -51,6 +52,7 @@ export default (store) => { { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, + { name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, { name: 'remote-user-profile-acct', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue @@ -11,14 +11,14 @@ <template v-if="relationship.following"> <button v-if="relationship.showing_reblogs" - class="btn button-default dropdown-item" + class="dropdown-item menu-item" @click="hideRepeats" > {{ $t('user_card.hide_repeats') }} </button> <button v-if="!relationship.showing_reblogs" - class="btn button-default dropdown-item" + class="dropdown-item menu-item" @click="showRepeats" > {{ $t('user_card.show_repeats') }} @@ -31,34 +31,34 @@ <UserListMenu :user="user" /> <button v-if="relationship.followed_by" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="removeUserFromFollowers" > {{ $t('user_card.remove_follower') }} </button> <button v-if="relationship.blocking" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="unblockUser" > {{ $t('user_card.unblock') }} </button> <button v-else - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="blockUser" > {{ $t('user_card.block') }} </button> <button - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="reportUser" > {{ $t('user_card.report') }} </button> <button v-if="pleromaChatMessagesAvailable" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="openChat" > {{ $t('user_card.message') }} @@ -122,19 +122,12 @@ <script src="./account_actions.js"></script> <style lang="scss"> -@import "../../variables"; - .AccountActions { .ellipsis-button { width: 2.5em; margin: -0.5em 0; padding: 0.5em 0; text-align: center; - - &:not(:hover) .icon { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } } </style> diff --git a/src/components/alert.style.js b/src/components/alert.style.js @@ -0,0 +1,51 @@ +export default { + name: 'Alert', + selector: '.alert', + validInnerComponents: [ + 'Text', + 'Icon', + 'Link', + 'Border', + 'ButtonUnstyled' + ], + variants: { + normal: '.neutral', + error: '.error', + warning: '.warning', + success: '.success' + }, + defaultRules: [ + { + directives: { + background: '--text', + opacity: 0.5, + blur: '9px' + } + }, + { + parent: { + component: 'Alert' + }, + component: 'Border', + textColor: '--parent' + }, + { + variant: 'error', + directives: { + background: '--cRed' + } + }, + { + variant: 'warning', + directives: { + background: '--cOrange' + } + }, + { + variant: 'success', + directives: { + background: '--cGreen' + } + } + ] +} diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue @@ -99,16 +99,14 @@ <script src="./announcement.js"></script> <style lang="scss"> -@import "../../variables"; - .announcement { - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); border-radius: 0; - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); .heading, .body { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .footer { diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue @@ -3,7 +3,7 @@ <textarea ref="textarea" v-model="announcement.content" - class="post-textarea" + class="input post-textarea" rows="1" cols="1" :placeholder="$t('announcements.post_placeholder')" @@ -14,6 +14,7 @@ <input id="announcement-start-time" v-model="announcement.startsAt" + class="input" :type="announcement.allDay ? 'date' : 'datetime-local'" :disabled="disabled" > @@ -23,6 +24,7 @@ <input id="announcement-end-time" v-model="announcement.endsAt" + class="input" :type="announcement.allDay ? 'date' : 'datetime-local'" :disabled="disabled" > diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue @@ -61,15 +61,13 @@ <script src="./announcements_page.js"></script> <style lang="scss"> -@import "../../variables"; - .announcements-page { .post-form { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); .heading, .body { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .post-button { diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Attachment { display: inline-flex; flex-direction: column; @@ -9,10 +7,8 @@ height: 100%; border-style: solid; border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-radius: var(--roundness); + border-color: var(--border); .attachment-wrapper { flex: 1 1 auto; @@ -84,6 +80,13 @@ } } + .video-container { + border: none; + outline: none; + color: inherit; + background: transparent; + } + .audio-container { display: flex; align-items: flex-end; @@ -126,23 +129,12 @@ .attachment-button { padding: 0; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); text-align: center; width: 2em; height: 2em; margin-left: 0.5em; font-size: 1.25em; - // TODO: theming? hard to theme with unknown background image color - background: rgb(230 230 230 / 70%); - - .svg-inline--fa { - color: rgb(0 0 0 / 60%); - } - - &:hover .svg-inline--fa { - color: rgb(0 0 0 / 90%); - } } } @@ -217,8 +209,7 @@ &.-placeholder { display: inline-block; - color: $fallback--link; - color: var(--postLink, $fallback--link); + color: var(--link); overflow: hidden; white-space: nowrap; height: auto; diff --git a/src/components/attachment/attachment.style.js b/src/components/attachment/attachment.style.js @@ -0,0 +1,24 @@ +export default { + name: 'Attachment', + selector: '.Attachment', + validInnerComponents: [ + 'Border', + 'ButtonUnstyled', + 'Input' + ], + defaultRules: [ + { + directives: { + roundness: 3 + } + }, + { + component: 'ButtonUnstyled', + parent: { component: 'Attachment' }, + directives: { + background: '#FFFFFF', + opacity: 0.5 + } + } + ] +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue @@ -38,7 +38,7 @@ v-if="edit" v-model="localDescription" type="text" - class="description-field" + class="input description-field" :placeholder="$t('post_status.media_description')" @keydown.enter.prevent="" > @@ -175,7 +175,6 @@ :is="videoTag" v-if="type === 'video' && !hidden" class="video-container" - :class="{ 'button-unstyled': 'isModal' }" :href="attachment.url" @click.stop.prevent="openModal" > @@ -253,7 +252,7 @@ v-if="edit" v-model="localDescription" type="text" - class="description-field" + class="input description-field" :placeholder="$t('post_status.media_description')" @keydown.enter.prevent="" > diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue @@ -1,3 +1,4 @@ +<!-- FIXME THIS NEEDS TO BE REFACTORED TO USE POPOVER --> <template> <div v-click-outside="onClickOutside" @@ -6,12 +7,12 @@ <input v-model="term" :placeholder="placeholder" - class="autosuggest-input" + class="input autosuggest-input" @click="onInputClick" > <div v-if="resultsVisible && filtered.length > 0" - class="autosuggest-results" + class="panel autosuggest-results" > <slot v-for="item in filtered" @@ -24,8 +25,6 @@ <script src="./autosuggest.js"></script> <style lang="scss"> -@import "../../variables"; - .autosuggest { position: relative; @@ -40,18 +39,15 @@ top: 100%; right: 0; max-height: 400px; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + background-color: var(--bg); border-style: solid; border-width: 1px; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border-color: var(--border); + border-radius: var(--roundness); border-top-left-radius: 0; border-top-right-radius: 0; box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); overflow-y: auto; z-index: 1; } diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue @@ -17,8 +17,6 @@ <script src="./avatar_list.js"></script> <style lang="scss"> -@import "../../variables"; - .avatars { display: flex; margin: 0; @@ -36,8 +34,7 @@ } .avatar-small { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); height: 24px; width: 24px; } diff --git a/src/components/badge.style.js b/src/components/badge.style.js @@ -0,0 +1,30 @@ +export default { + name: 'Badge', + selector: '.badge', + validInnerComponents: [ + 'Text', + 'Icon' + ], + variants: { + notification: '.-notification' + }, + defaultRules: [ + { + component: 'Root', + directives: { + '--badgeNotification': 'color | --cRed' + } + }, + { + directives: { + background: '--cGreen' + } + }, + { + variant: 'notification', + directives: { + background: '--cRed' + } + } + ] +} diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue @@ -47,7 +47,6 @@ display: flex; flex: 1 0; margin: 0; - padding: 0.6em 1em; --emoji-size: 14px; diff --git a/src/components/border.style.js b/src/components/border.style.js @@ -0,0 +1,13 @@ +export default { + name: 'Border', + selector: '/*border*/', + virtual: true, + defaultRules: [ + { + directives: { + textColor: '$mod(--parent, 10)', + textAuto: 'no-auto' + } + } + ] +} diff --git a/src/components/button.style.js b/src/components/button.style.js @@ -0,0 +1,101 @@ +export default { + name: 'Button', // Name of the component + selector: '.button-default', // CSS selector/prefix + // outOfTreeSelector: '' // out-of-tree selector is used when other components are laid over it but it's not part of the tree, see Underlay component + // States, system witll calculate ALL possible combinations of those and prepend "normal" to them + standalone "normal" state + states: { + // States are a bit expensive - the amount of combinations generated is about (1/6)n^3+n, so adding more state increased number of combination by an order of magnitude! + // All states inherit from "normal" state, there is no other inheirtance, i.e. hover+disabled only inherits from "normal", not from hover nor disabled. + // However, cascading still works, so resulting state will be result of merging of all relevant states/variants + // normal: '' // normal state is implicitly added, it is always included + toggled: '.toggled', + pressed: ':active', + hover: ':hover:not(:disabled)', + focused: ':focus-within', + disabled: ':disabled' + }, + // Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it. + variants: { + // Variants save on computation time since adding new variant just adds one more "set". + // normal: '', // you can override normal variant, it will be appenended to the main class + danger: '.danger' + // Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants. + // This (currently) is further multipled by number of places where component can exist. + }, + // This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever). + validInnerComponents: [ + 'Text', + 'Icon' + ], + // Default rules, used as "default theme", essentially. + defaultRules: [ + { + component: 'Root', + 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)' + } + }, + { + // component: 'Button', // no need to specify components every time unless you're specifying how other component should look + // like within it + directives: { + background: '--fg', + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] + } + }, + { + state: ['pressed'], + directives: { + shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] + } + }, + { + state: ['hover', 'pressed'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel'] + } + }, + { + state: ['toggled'], + directives: { + background: '--inheritedBackground,-14.2', + shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] + } + }, + { + state: ['toggled', 'hover'], + directives: { + background: '--inheritedBackground,-14.2', + shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel'] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: ['--defaultButtonBevel'] + } + }, + { + component: 'Text', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js @@ -0,0 +1,96 @@ +export default { + name: 'ButtonUnstyled', + selector: '.button-unstyled', + states: { + toggled: '.toggled', + disabled: ':disabled', + hover: ':hover:not(:disabled)', + focused: ':focus-within' + }, + validInnerComponents: [ + 'Text', + 'Icon', + 'Badge' + ], + defaultRules: [ + { + directives: { + background: '#ffffff', + opacity: 0, + shadow: [] + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'focused'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'focused', 'hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Text', + parent: { + component: 'ButtonUnstyled', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss @@ -11,15 +11,15 @@ .chat-view-body { box-sizing: border-box; - background-color: var(--chatBg, $fallback--bg); display: flex; flex-direction: column; width: 100%; overflow: visible; min-height: calc(100vh - var(--navbar-height)); margin: 0; - border-radius: 10px 10px 0 0; - border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0; + border-radius: var(--roundness); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; &::after { border-radius: 0; @@ -37,8 +37,6 @@ .footer { position: sticky; bottom: 0; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); z-index: 1; } @@ -61,8 +59,6 @@ position: absolute; right: 1.3em; top: -3.2em; - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); display: flex; justify-content: center; align-items: center; @@ -79,12 +75,6 @@ visibility: visible; } - i { - font-size: 1em; - color: $fallback--text; - color: var(--text, $fallback--text); - } - .unread-message-count { font-size: 0.8em; left: 50%; diff --git a/src/components/chat/chat.style.js b/src/components/chat/chat.style.js @@ -0,0 +1,19 @@ +export default { + name: 'Chat', + selector: '.chat-message-list', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Avatar', + 'ChatMessage' + ], + defaultRules: [ + { + directives: { + background: '--bg', + blur: '5px' + } + } + ] +} diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue @@ -26,7 +26,7 @@ </div> </div> <div - class="message-list" + class="chat-message-list message-list" :style="{ height: scrollableContainerHeight }" > <template v-if="!errorLoadingChat"> @@ -61,7 +61,7 @@ <FAIcon icon="chevron-down" /> <div v-if="newMessageCount" - class="badge badge-notification unread-chat-count unread-message-count" + class="badge -notification unread-chat-count unread-message-count" > {{ newMessageCount }} </div> @@ -95,6 +95,5 @@ <script src="./chat.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat"; </style> diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue @@ -45,8 +45,6 @@ <script src="./chat_list.js"></script> <style lang="scss"> -@import "../../variables"; - .chat-list { min-height: 25em; margin-bottom: 0; @@ -57,8 +55,7 @@ font-size: 1.2em; display: flex; justify-content: center; - color: $fallback--text; - color: var(--faint, $fallback--text); + color: var(--textFaint); } </style> diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss @@ -1,8 +1,6 @@ .chat-list-item { display: flex; flex-direction: row; - padding: 0.75em; - height: 5em; overflow: hidden; box-sizing: border-box; cursor: pointer; @@ -11,11 +9,6 @@ outline: none; } - &:hover { - background-color: var(--selectedPost, $fallback--lightBg); - box-shadow: 0 0 3px 1px rgb(0 0 0 / 10%); - } - .chat-list-item-left { margin-right: 1em; } @@ -29,7 +22,7 @@ .heading { width: 100%; - display: inline-flex; + display: flex; justify-content: space-between; line-height: 1em; } @@ -47,18 +40,17 @@ } .chat-preview { - display: inline-flex; + display: flex; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin: 0.35em 0; - color: $fallback--text; - color: var(--faint, $fallback--text); + color: var(--textFaint); width: 100%; } a { - color: var(--faintLink, $fallback--link); + color: var(--linkFaint); text-decoration: none; pointer-events: none; } @@ -73,11 +65,6 @@ } } - .Avatar { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } - .chat-preview-body { --emoji-size: 1.4em; diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue @@ -36,7 +36,7 @@ /> <div v-if="chat.unread > 0" - class="badge badge-notification unread-chat-count" + class="badge -notification unread-chat-count" > {{ chat.unread }} </div> @@ -48,6 +48,5 @@ <script src="./chat_list_item.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat_list_item"; </style> diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .chat-message-wrapper { &.hovered-message-chain { .animated.Avatar { @@ -27,12 +25,6 @@ .menu-icon { cursor: pointer; - - &:hover, - .extra-button-popover.open & { - color: $fallback--text; - color: var(--text, $fallback--text); - } } .popover { @@ -61,10 +53,12 @@ } .status { - border-radius: $fallback--chatMessageRadius; - border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); + background-color: var(--background); + color: var(--text); + border-radius: var(--roundness); display: flex; padding: 0.75em; + border: 1px solid var(--border); } .created-at { @@ -97,8 +91,7 @@ .error { .status-content.media-body, .created-at { - color: $fallback--cRed; - color: var(--badgeNotification, $fallback--cRed); + color: var(--badgeNotification); } } @@ -117,16 +110,6 @@ align-content: end; justify-content: flex-end; - a { - color: var(--chatMessageOutgoingLink, $fallback--link); - } - - .status { - color: var(--chatMessageOutgoingText, $fallback--text); - background-color: var(--chatMessageOutgoingBg, $fallback--lightBg); - border: 1px solid var(--chatMessageOutgoingBorder, --lightBg); - } - .chat-message-inner { align-items: flex-end; } @@ -137,22 +120,6 @@ } .incoming { - a { - color: var(--chatMessageIncomingLink, $fallback--link); - } - - .status { - color: var(--chatMessageIncomingText, $fallback--text); - background-color: var(--chatMessageIncomingBg, $fallback--bg); - border: 1px solid var(--chatMessageIncomingBorder, --border); - } - - .created-at { - a { - color: var(--chatMessageIncomingText, $fallback--text); - } - } - .chat-message-menu { left: 0.4rem; } @@ -176,6 +143,5 @@ margin: 1.4em 0; font-size: 0.9em; user-select: none; - color: $fallback--text; - color: var(--faintedText, $fallback--text); + color: var(--textFaint); } diff --git a/src/components/chat_message/chat_message.style.js b/src/components/chat_message/chat_message.style.js @@ -0,0 +1,30 @@ +export default { + name: 'ChatMessage', + selector: '.chat-message', + variants: { + outgoing: '.outgoing' + }, + validInnerComponents: [ + 'Text', + 'Icon', + 'Border', + 'Button', + 'RichContent', + 'Attachment', + 'PollGraph' + ], + defaultRules: [ + { + directives: { + background: '--bg, 2', + backgroundNoCssColor: 'yes' + } + }, + { + variant: 'outgoing', + directives: { + background: '--bg, 5' + } + } + ] +} diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue @@ -53,7 +53,7 @@ <template #content> <div class="dropdown-menu"> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click="deleteMessage" > <FAIcon icon="times" /> {{ $t("chats.delete") }} diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss @@ -16,11 +16,6 @@ padding-bottom: 0.7rem; } - .basic-user-card:hover { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); - } - .go-back-button { text-align: center; line-height: 1; diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue @@ -16,27 +16,29 @@ /> </button> </div> - <div class="input-wrap"> - <div class="input-search"> - <FAIcon - class="search-icon fa-scale-110 fa-old-padding" - icon="search" - /> + <div class="panel-body"> + <div class="input-wrap"> + <div class="input-search"> + <FAIcon + class="search-icon fa-scale-110 fa-old-padding" + icon="search" + /> + </div> + <input + ref="search" + v-model="query" + class="input" + placeholder="Search people" + @input="onInput" + > </div> - <input - ref="search" - v-model="query" - placeholder="Search people" - @input="onInput" - > - </div> - <div class="member-list"> - <div - v-for="user in availableUsers" - :key="user.id" - class="member" - > - <div @click.capture.prevent="goToChat(user)"> + <div class="member-list"> + <div + v-for="user in availableUsers" + :key="user.id" + class="list-item" + @click.capture.prevent="goToChat(user)" + > <BasicUserCard :user="user" /> </div> </div> @@ -46,6 +48,5 @@ <script src="./chat_new.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat_new"; </style> diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue @@ -26,8 +26,6 @@ <script src="./chat_title.js"></script> <style lang="scss"> -@import "../../variables"; - .chat-title { display: flex; overflow: hidden; @@ -54,8 +52,7 @@ margin-right: 0.5em; height: 1.5em; width: 1.5em; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); &.animated::before { display: none; diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue @@ -12,7 +12,7 @@ @change="$emit('update:modelValue', $event.target.checked)" > <i - class="checkbox-indicator" + class="input -checkbox checkbox-indicator" :aria-hidden="true" @transitionend.capture="onTransitionEnd" /> @@ -54,7 +54,6 @@ export default { </script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .checkbox { @@ -62,9 +61,15 @@ export default { display: inline-block; min-height: 1.2em; - &-indicator { + & > &-indicator { + /* Reset .input stuff */ + padding: 0; + margin: 0; position: relative; + line-height: inherit; + display: inline; padding-left: 1.2em; + box-shadow: none; } &-indicator::before { @@ -76,12 +81,9 @@ export default { transition: color 200ms; width: 1.1em; height: 1.1em; - border-radius: $fallback--checkboxRadius; - border-radius: var(--checkboxRadius, $fallback--checkboxRadius); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); + border-radius: var(--roundness); + box-shadow: var(--shadow); + background-color: var(--background); vertical-align: top; text-align: center; line-height: 1.1em; @@ -98,21 +100,18 @@ export default { } .label { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--text); } } input[type="checkbox"] { &:checked + .checkbox-indicator::before { - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); } &:indeterminate + .checkbox-indicator::before { content: "–"; - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); } } diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .color-input { display: inline-flex; @@ -11,9 +9,8 @@ padding: 0.2em 8px; input { + color: var(--text); background: none; - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); border: none; padding: 0; margin: 0; @@ -23,21 +20,38 @@ min-width: 3em; padding: 0; } + } + + .nativeColor { + cursor: pointer; + flex: 0 0 auto; - &.nativeColor { - flex: 0 0 2em; - min-width: 2em; - align-self: stretch; - min-height: 100%; + input { + appearance: none; + max-width: 0; + min-width: 0; + max-height: 0; + /* stylelint-disable-next-line declaration-no-important */ + opacity: 0 !important; } } .computedIndicator, + .validIndicator, + .invalidIndicator, .transparentIndicator { flex: 0 0 2em; + margin: 0 0.5em; min-width: 2em; align-self: stretch; - min-height: 100%; + min-height: 1.5em; + border-radius: var(--roundness); + } + + .invalidIndicator { + background: transparent; + box-sizing: border-box; + border: 2px solid var(--cRed); } .transparentIndicator { @@ -58,11 +72,13 @@ &::after { top: 0; left: 0; + border-top-left-radius: var(--roundness); } &::before { bottom: 0; right: 0; + border-bottom-right-radius: var(--roundness); } } } diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue @@ -25,30 +25,51 @@ :disabled="!present || disabled" @input="$emit('update:modelValue', $event.target.value)" > - <input + <div v-if="validColor" - :id="name" - class="nativeColor unstyled" - type="color" - :value="modelValue || fallback" - :disabled="!present || disabled" - @input="$emit('update:modelValue', $event.target.value)" - > + class="validIndicator" + :style="{backgroundColor: modelValue || fallback}" + /> <div - v-if="transparentColor" + v-else-if="transparentColor" class="transparentIndicator" /> <div - v-if="computedColor" + v-else-if="computedColor" class="computedIndicator" :style="{backgroundColor: fallback}" /> + <div + v-else + class="invalidIndicator" + /> + <label class="nativeColor"> + <FAIcon icon="eye-dropper" /> + <input + :id="name" + class="unstyled" + type="color" + :value="modelValue || fallback" + :disabled="!present || disabled" + @input="$emit('update:modelValue', $event.target.value)" + > + </label> </div> </div> </template> <script> import Checkbox from '../checkbox/checkbox.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEyeDropper +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEyeDropper +) + export default { components: { Checkbox @@ -108,12 +129,3 @@ export default { } </script> <style lang="scss" src="./color_input.scss"></style> - -<style lang="scss"> -.color-control { - input.text-input { - max-width: 7em; - flex: 1; - } -} -</style> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js @@ -56,7 +56,8 @@ const conversation = { expanded: false, threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' statusContentPropertiesObject: {}, - inlineDivePosition: null + inlineDivePosition: null, + loadStatusError: null } }, props: [ @@ -392,11 +393,15 @@ const conversation = { this.setHighlight(this.originalStatusId) }) } else { + this.loadStatusError = null this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) .then((status) => { this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.fetchConversation() }) + .catch((error) => { + this.loadStatusError = error + }) } }, getReplies (id) { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -28,7 +28,27 @@ class="rightside-button" /> </div> - <div class="conversation-body panel-body"> + <div + v-if="isPage && !status" + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > + <p v-if="!loadStatusError"> + <FAIcon + spin + icon="circle-notch" + /> + {{ $t('status.loading') }} + </p> + <p v-else> + {{ $t('status.load_error', { error: loadStatusError }) }} + </p> + </div> + <div + v-else + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > <div v-if="isTreeView" class="thread-body" @@ -203,6 +223,7 @@ </div> <div v-else + class="Conversation -hidden" :style="hiddenStyle" /> </template> @@ -210,14 +231,17 @@ <script src="./conversation.js"></script> <style lang="scss"> -@import "../../variables"; - .Conversation { z-index: 1; + &.-hidden { + background: var(--__panel-background); + backdrop-filter: var(--__panel-backdrop-filter); + } + .conversation-dive-to-top-level-box { - padding: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -227,20 +251,22 @@ } .thread-ancestors { - margin-left: var(--status-margin, $status-margin); - border-left: 2px solid var(--border, $fallback--border); + margin-left: var(--status-margin); + border-left: 2px solid var(--border); } - .thread-ancestor.-faded .StatusContent { - --link: var(--faintLink); - --text: var(--faint); - - color: var(--text); + .thread-ancestor.-faded .RichContent { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ } .thread-ancestor-dive-box { - padding-left: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding-left: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -253,16 +279,17 @@ } .thread-ancestor-dive-box-inner { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); } .conversation-status { - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); border-radius: 0; } .thread-ancestor-has-other-replies .conversation-status, - &:last-child .conversation-status, + &:last-child:not(.-expanded) .conversation-status, + &.-expanded .conversation-status:last-child, .thread-ancestor:last-child .conversation-status, .thread-ancestor:last-child .thread-ancestor-dive-box, &.-expanded .thread-tree .conversation-status { @@ -270,20 +297,36 @@ } .thread-ancestors + .thread-tree > .conversation-status { - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } /* expanded conversation in timeline */ &.status-fadein.-expanded .thread-body { - border-left: 4px solid $fallback--cRed; - border-left-color: var(--cRed, $fallback--cRed); - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); - border-bottom: 1px solid var(--border, $fallback--border); + border-left: 4px solid var(--cRed); + border-radius: var(--roundness); + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom: 1px solid var(--border); } &.-expanded.status-fadein { - margin: calc(var(--status-margin, $status-margin) / 2); + --___margin: calc(var(--status-margin) / 2); + + background: var(--background); + margin: var(--___margin); + + &::before { + z-index: -1; + content: ""; + display: block; + position: absolute; + top: calc(var(--___margin) * -1); + bottom: calc(var(--___margin) * -1); + left: calc(var(--___margin) * -1); + right: calc(var(--___margin) * -1); + background: var(--background); + backdrop-filter: var(--__panel-backdrop-filter); + } } } </style> diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .DesktopNav { width: 100%; z-index: var(--ZI_navbar); @@ -9,7 +7,7 @@ } a { - color: var(--topBarLink, $fallback--link); + color: var(--link); } .inner-nav { @@ -54,27 +52,7 @@ .button-default { &, svg { - color: $fallback--text; - color: var(--btnTopBarText, $fallback--text); - } - - &:active { - background-color: $fallback--fg; - background-color: var(--btnPressedTopBar, $fallback--fg); - color: $fallback--text; - color: var(--btnPressedTopBarText, $fallback--text); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledTopBarText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledTopBarText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnToggledTopBar, $fallback--fg); + color: var(--text); } } @@ -94,8 +72,7 @@ mask-repeat: no-repeat; mask-position: center; mask-size: contain; - background-color: $fallback--fg; - background-color: var(--topBarText, $fallback--fg); + background-color: var(--text); position: absolute; top: 0; bottom: 0; @@ -116,8 +93,7 @@ text-align: center; .svg-inline--fa { - color: $fallback--link; - color: var(--topBarLink, $fallback--link); + color: var(--link); } } diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue @@ -12,7 +12,7 @@ <slot name="header" /> </div> </div> - <div class="dialog-modal-content"> + <div class="panel-body dialog-modal-content"> <slot name="default" /> </div> <div class="dialog-modal-footer user-interactions panel-footer"> @@ -25,8 +25,6 @@ <script src="./dialog_modal.js"></script> <style lang="scss"> -@import "../../variables"; - // TODO: unify with other modals. .dark-overlay { &::before { @@ -54,8 +52,6 @@ z-index: 2001; cursor: default; display: block; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); .dialog-modal-heading { .title { @@ -66,18 +62,13 @@ .dialog-modal-content { margin: 0; padding: 1rem; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); white-space: normal; } .dialog-modal-footer { margin: 0; padding: 0.5em; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-top: 1px solid $fallback--border; - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); display: flex; justify-content: flex-end; diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -1,7 +1,7 @@ <template> <div ref="root" - class="emoji-input" + class="input emoji-input" :class="{ 'with-picker': !hideEmojiButton }" > <slot @@ -68,9 +68,9 @@ v-for="(suggestion, index) in suggestions" :id="suggestionItemId(index)" :key="index" - class="autocomplete-item" + class="menu-item autocomplete-item" role="option" - :class="{ highlighted: index === highlighted }" + :class="{ '-active': index === highlighted }" :aria-label="autoCompleteItemLabel(suggestion)" :aria-selected="index === highlighted" @click.stop.prevent="onClick($event, suggestion)" @@ -110,9 +110,8 @@ <script src="./emoji_input.js"></script> <style lang="scss"> -@import "../../variables"; - -.emoji-input { +.input.emoji-input { + padding: 0; display: flex; flex-direction: column; position: relative; @@ -127,8 +126,7 @@ line-height: 24px; &:hover i { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } @@ -145,6 +143,12 @@ input, textarea { flex: 1 0 auto; + color: inherit; + /* stylelint-disable-next-line declaration-no-important */ + background: none !important; + box-shadow: none; + border: none; + outline: none; } &.with-picker input { @@ -179,26 +183,28 @@ position: absolute; } - &-item { + &-item.menu-item { display: flex; - cursor: pointer; - padding: 0.2em 0.4em; - border-bottom: 1px solid rgb(0 0 0 / 40%); - height: 32px; + padding-top: 0; + padding-bottom: 0; .image { - width: 32px; - height: 32px; - line-height: 32px; + width: calc(var(--__line-height) + var(--__vertical-gap) * 2); + height: calc(var(--__line-height) + var(--__vertical-gap) * 2); + line-height: var(--__line-height); text-align: center; - font-size: 32px; - margin-right: 4px; + margin-right: var(--__horizontal-gap); img { - width: 32px; - height: 32px; + width: calc(var(--__line-height) + var(--__vertical-gap) * 2); + height: calc(var(--__line-height) + var(--__vertical-gap) * 2); object-fit: contain; } + + span { + font-size: var(--__line-height); + line-height: var(--__line-height); + } } .label { @@ -216,17 +222,6 @@ line-height: 9px; } } - - &.highlighted { - background-color: $fallback--fg; - background-color: var(--selectedMenuPopover, $fallback--fg); - color: var(--selectedMenuPopoverText, $fallback--text); - - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } } } </style> diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js @@ -114,11 +114,13 @@ const EmojiPicker = { groupsScrolledClass: 'scrolled-top', keepOpen: false, customEmojiTimeout: null, + hideCustomEmojiInPicker: false, // Lazy-load only after the first time `showing` becomes true. contentLoaded: false, groupRefs: {}, emojiRefs: {}, filteredEmojiGroups: [], + emojiSize: 0, width: 0 } }, @@ -129,6 +131,23 @@ const EmojiPicker = { Popover }, methods: { + updateEmojiSize () { + const css = window.getComputedStyle(this.$refs.popover.$el) + const emojiSize = css.getPropertyValue('--emojiSize') + const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '') + const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, '')) + const fontSize = css.getPropertyValue('font-size').replace(/[^0-9,.]+/, '') + + let emojiSizeReal + if (emojiSizeUnit.endsWith('em')) { + emojiSizeReal = emojiSizeValue * fontSize + } else { + emojiSizeReal = emojiSizeValue + } + + const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSize) + this.emojiSize = fullEmojiSize + }, showPicker () { this.$refs.popover.showPopover() this.onShowing() @@ -223,6 +242,7 @@ const EmojiPicker = { }, onShowing () { const oldContentLoaded = this.contentLoaded + this.updateEmojiSize() this.recalculateItemPerRow() this.$nextTick(() => { this.$refs.search.focus() @@ -265,16 +285,20 @@ const EmojiPicker = { }, computed: { minItemSize () { - return this.emojiHeight + return this.emojiSize }, - emojiHeight () { - return 32 + 4 + // used to watch it + fontSize () { + this.$nextTick(() => { + this.updateEmojiSize() + }) + return this.$store.getters.mergedConfig.fontSize }, - emojiWidth () { - return 32 + 4 + emojiHeight () { + return this.emojiSize }, itemPerRow () { - return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6 + return this.width ? Math.floor(this.width / this.emojiSize) : 6 }, activeGroupView () { return this.showingStickers ? '' : this.activeGroup @@ -286,7 +310,7 @@ const EmojiPicker = { return 0 }, allCustomGroups () { - if (this.hideCustomEmoji) { + if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) { return {} } const emojis = this.$store.getters.groupedCustomEmojis diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss @@ -1,62 +1,55 @@ -@import "../../variables"; - -$emoji-picker-header-height: 36px; -$emoji-picker-header-picture-width: 32px; -$emoji-picker-header-picture-height: 32px; -$emoji-picker-emoji-size: 32px; - .emoji-picker { + --__emoji-picker-header: 2.2em; + width: 25em; max-width: calc(100vw - 20px); // popover gives 10px margin from window edge display: flex; flex-direction: column; - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--link; - color: var(--popoverText, $fallback--link); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --icon: var(--popoverIcon, $fallback--icon); &-header-image { display: inline-flex; justify-content: center; align-items: center; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); .still-image { - max-width: 100%; - max-height: 100%; - height: 100%; - width: 100%; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); object-fit: contain; + + --_still_image-label-scale: 0.5; } } .keep-open, - .too-many-emoji { - padding: 7px; + .too-many-emoji, + .hide-custom-emoji { + padding: 0.5em; line-height: normal; } + .hide-custom-emoji { + padding-top: 0; + } + .too-many-emoji { display: flex; flex-direction: column; } .keep-open-label { - padding: 0 7px; + padding: 0 0.5em; display: flex; } .heading { display: flex; - padding: 10px 7px 5px; + padding: 0.7em 0.5em 0; } .content { @@ -71,14 +64,14 @@ $emoji-picker-emoji-size: 32px; display: flex; flex-flow: row nowrap; overflow-x: auto; + overflow-y: hidden; } .additional-tabs { display: flex; border-left: 1px solid; - border-left-color: $fallback--icon; - border-left-color: var(--icon, $fallback--icon); - padding-left: 7px; + border-left-color: var(--border); + padding-left: 0.5em; flex: 0 0 auto; } @@ -87,30 +80,29 @@ $emoji-picker-emoji-size: 32px; flex-basis: auto; display: flex; align-content: center; + scrollbar-width: thin; &-item { - padding: 0 7px; + padding: 0 0.5em; cursor: pointer; - font-size: 1.85em; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); display: flex; align-items: center; + .svg-inline--fa { + font-size: 1.85em; + } + &.disabled { opacity: 0.5; pointer-events: none; } - &.active { - border-bottom: 4px solid; - - svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } + &.toggled { + border-bottom: 0.2em solid; } } } @@ -137,7 +129,7 @@ $emoji-picker-emoji-size: 32px; .emoji { &-search { - padding: 5px; + padding: 0.3em; flex: 0 0 auto; input { @@ -151,6 +143,7 @@ $emoji-picker-emoji-size: 32px; flex: 1 1 1px; position: relative; overflow: auto; + scrollbar-gutter: stable both-edges; user-select: none; mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, @@ -177,13 +170,13 @@ $emoji-picker-emoji-size: 32px; display: flex; align-items: center; flex-wrap: wrap; - padding-left: 5px; justify-content: left; &-title { font-size: 0.85em; width: 100%; margin: 0; + padding-left: 0.3em; &.disabled { display: none; @@ -192,24 +185,28 @@ $emoji-picker-emoji-size: 32px; } &-item { - width: $emoji-picker-emoji-size; - height: $emoji-picker-emoji-size; + width: var(--emoji-size); + height: var(--emoji-size); box-sizing: border-box; display: flex; - line-height: $emoji-picker-emoji-size; + line-height: var(--emoji-size); align-items: center; justify-content: center; - margin: 4px; + margin: 0.2em; cursor: pointer; .emoji-picker-emoji.-custom { object-fit: contain; - max-width: 100%; - max-height: 100%; + width: var(--emoji-size); + max-width: var(--emoji-size); + height: var(--emoji-size); + max-height: var(--emoji-size); + + --_still_image-label-scale: 0.5; } .emoji-picker-emoji.-unicode { - font-size: 24px; + font-size: 1.6em; overflow: hidden; } } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue @@ -23,9 +23,9 @@ v-for="group in filteredEmojiGroups" :ref="setGroupRef('group-header-' + group.id)" :key="group.id" - class="emoji-tabs-item" + class="button-unstyled emoji-tabs-item" :class="{ - active: activeGroupView === group.id + toggled: activeGroupView === group.id }" :title="group.text" role="button" @@ -52,8 +52,8 @@ class="additional-tabs" > <span - class="stickers-tab-icon additional-tabs-item" - :class="{active: showingStickers}" + class="button-unstyled stickers-tab-icon additional-tabs-item" + :class="{toggled: showingStickers}" :title="$t('emoji.stickers')" @click.prevent="toggleStickers" > @@ -77,7 +77,7 @@ ref="search" v-model="keyword" type="text" - class="form-control" + class="input form-control" :placeholder="$t('emoji.search_emoji')" @input="$event.target.composing = false" > @@ -142,6 +142,17 @@ {{ $t('emoji.keep_open') }} </Checkbox> </div> + <div + v-if="!hideCustomEmoji" + class="hide-custom-emoji" + > + <Checkbox + v-model="hideCustomEmojiInPicker" + @change="onShowing" + > + {{ $t('emoji.hide_custom_emoji') }} + </Checkbox> + </div> </div> <div v-if="showingStickers" diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue @@ -72,7 +72,6 @@ <script src="./emoji_reactions.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .EmojiReactions { @@ -80,7 +79,7 @@ margin-top: 0.25em; flex-wrap: wrap; - --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1)); + --emoji-size: calc(var(--emojiSize, 1.25em) * var(--emojiReactionsScale, 1)); .emoji-reaction-container { display: flex; @@ -92,7 +91,6 @@ padding: 0; .emoji-reaction-count-button { - background-color: var(--btn); margin: 0; height: 100%; border-top-left-radius: 0; @@ -102,11 +100,9 @@ display: inline-flex; justify-content: center; align-items: center; - color: $fallback--text; - color: var(--btnText, $fallback--text); &.-picked-reaction { - border: 1px solid var(--accent, $fallback--link); + border: 1px solid var(--accent); margin-right: -1px; } } @@ -149,18 +145,16 @@ } .svg-inline--fa { - color: $fallback--text; - color: var(--btnText, $fallback--text); + color: var(--text); } &.-picked-reaction { - border: 1px solid var(--accent, $fallback--link); + border: 1px solid var(--accent); margin-left: -1px; // offset the border, can't use inset shadows either margin-right: -1px; .svg-inline--fa { - color: $fallback--link; - color: var(--accent, $fallback--link); + color: var(--accent); } } @@ -176,8 +170,7 @@ @include focused-style { .svg-inline--fa { - color: $fallback--link; - color: var(--accent, $fallback--link); + color: var(--accent); } .focus-marker { diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue @@ -12,13 +12,13 @@ > <template #content="{close}"> <div + :id="`popup-menu-${randomSeed}`" class="dropdown-menu" role="menu" - :id="`popup-menu-${randomSeed}`" > <button v-if="canMute && !status.thread_muted" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="muteConversation" > @@ -29,7 +29,7 @@ </button> <button v-if="canMute && status.thread_muted" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="unmuteConversation" > @@ -40,7 +40,7 @@ </button> <button v-if="!status.pinned && canPin" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="pinStatus" @click="close" @@ -52,7 +52,7 @@ </button> <button v-if="status.pinned && canPin" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="unpinStatus" @click="close" @@ -65,7 +65,7 @@ <template v-if="canBookmark"> <button v-if="!status.bookmarked" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="bookmarkStatus" @click="close" @@ -77,7 +77,7 @@ </button> <button v-if="status.bookmarked" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="unbookmarkStatus" @click="close" @@ -90,7 +90,7 @@ </template> <button v-if="ownStatus && editingAvailable" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="editStatus" @click="close" @@ -102,7 +102,7 @@ </button> <button v-if="isEdited && editingAvailable" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="showStatusHistory" @click="close" @@ -114,7 +114,7 @@ </button> <button v-if="canDelete" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="deleteStatus" @click="close" @@ -125,7 +125,7 @@ /><span>{{ $t("status.delete") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="copyLink" @click="close" @@ -137,7 +137,7 @@ </button> <a v-if="!status.is_local" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" title="Source" :href="status.external_url" @@ -149,7 +149,7 @@ /><span>{{ $t("status.external_source") }}</span> </a> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="reportStatus" @click="close" @@ -201,7 +201,6 @@ <script src="./extra_buttons.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ExtraButtons { @@ -211,8 +210,7 @@ margin: -10px; &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } diff --git a/src/components/extra_notifications/extra_notifications.js b/src/components/extra_notifications/extra_notifications.js @@ -0,0 +1,48 @@ +import { mapGetters } from 'vuex' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUserPlus, + faComments, + faBullhorn +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUserPlus, + faComments, + faBullhorn +) + +const ExtraNotifications = { + computed: { + shouldShowChats () { + return this.mergedConfig.showExtraNotifications && this.mergedConfig.showChatsInExtraNotifications && this.unreadChatCount + }, + shouldShowAnnouncements () { + return this.mergedConfig.showExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications && this.unreadAnnouncementCount + }, + shouldShowFollowRequests () { + return this.mergedConfig.showExtraNotifications && this.mergedConfig.showFollowRequestsInExtraNotifications && this.followRequestCount + }, + hasAnythingToShow () { + return this.shouldShowChats || this.shouldShowAnnouncements || this.shouldShowFollowRequests + }, + shouldShowCustomizationTip () { + return this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow + }, + currentUser () { + return this.$store.state.users.currentUser + }, + ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'followRequestCount', 'mergedConfig']) + }, + methods: { + openNotificationSettings () { + return this.$store.dispatch('openSettingsModalTab', 'notifications') + }, + dismissConfigurationTip () { + return this.$store.dispatch('setOption', { name: 'showExtraNotificationsTip', value: false }) + } + } +} + +export default ExtraNotifications diff --git a/src/components/extra_notifications/extra_notifications.vue b/src/components/extra_notifications/extra_notifications.vue @@ -0,0 +1,110 @@ +<template> + <div class="ExtraNotifications"> + <div + v-if="shouldShowChats" + class="notification unseen" + > + <div class="notification-overlay" /> + <router-link + class="button-unstyled -link extra-notification" + :to="{ name: 'chats', params: { username: currentUser.screen_name } }" + > + <FAIcon + fixed-width + class="fa-scale-110 icon" + icon="comments" + /> + {{ $tc('notifications.unread_chats', unreadChatCount, { num: unreadChatCount }) }} + </router-link> + </div> + <div + v-if="shouldShowAnnouncements" + class="notification unseen" + > + <div class="notification-overlay" /> + <router-link + class="button-unstyled -link extra-notification" + :to="{ name: 'announcements' }" + > + <FAIcon + fixed-width + class="fa-scale-110 icon" + icon="bullhorn" + /> + {{ $tc('notifications.unread_announcements', unreadAnnouncementCount, { num: unreadAnnouncementCount }) }} + </router-link> + </div> + <div + v-if="shouldShowFollowRequests" + class="notification unseen" + > + <div class="notification-overlay" /> + <router-link + class="button-unstyled -link extra-notification" + :to="{ name: 'friend-requests' }" + > + <FAIcon + fixed-width + class="fa-scale-110 icon" + icon="user-plus" + /> + {{ $tc('notifications.unread_follow_requests', followRequestCount, { num: followRequestCount }) }} + </router-link> + </div> + <i18n-t + v-if="shouldShowCustomizationTip" + tag="span" + class="notification tip extra-notification" + keypath="notifications.configuration_tip" + > + <template #theSettings> + <button + class="button-unstyled -link" + @click="openNotificationSettings" + > + {{ $t('notifications.configuration_tip_settings') }} + </button> + </template> + <template #dismiss> + <button + class="button-unstyled -link" + @click="dismissConfigurationTip" + > + {{ $t('notifications.configuration_tip_dismiss') }} + </button> + </template> + </i18n-t> + </div> +</template> + +<script src="./extra_notifications.js" /> + +<style lang="scss"> +.ExtraNotifications { + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + + .notification { + width: 100%; + border-bottom: 1px solid; + border-color: var(--border); + display: flex; + flex-direction: column; + align-items: stretch; + } + + .extra-notification { + padding: 1em; + } + + .icon { + margin-right: 0.5em; + } + + .tip { + display: inline; + } +} +</style> diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue @@ -65,7 +65,6 @@ <script src="./favorite_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .FavoriteButton { @@ -88,8 +87,7 @@ &:hover .svg-inline--fa, &.-favorited .svg-inline--fa { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } @include unfocused-style { diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue @@ -42,8 +42,6 @@ <script src="./flash.js"></script> <style lang="scss"> -@import "../../variables"; - .Flash { display: inline-block; width: 100%; diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js @@ -1,63 +1,59 @@ -import { set } from 'lodash' import Select from '../select/select.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import Popover from 'src/components/popover/popover.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faExclamationTriangle, + faKeyboard, + faFont +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faExclamationTriangle, + faKeyboard, + faFont +) export default { components: { - Select + Select, + Checkbox, + Popover }, props: [ 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' ], + mounted () { + this.$store.dispatch('queryLocalFonts') + }, emits: ['update:modelValue'], data () { return { - lValue: this.modelValue, + manualEntry: false, availableOptions: [ this.noInherit ? '' : 'inherit', - 'custom', - ...(this.options || []), 'serif', + 'sans-serif', 'monospace', - 'sans-serif' + ...(this.options || []) ].filter(_ => _) } }, - beforeUpdate () { - this.lValue = this.modelValue + methods: { + toggleManualEntry () { + this.manualEntry = !this.manualEntry + } }, computed: { present () { - return typeof this.lValue !== 'undefined' - }, - dValue () { - return this.lValue || this.fallback || {} - }, - family: { - get () { - return this.dValue.family - }, - set (v) { - set(this.lValue, 'family', v) - this.$emit('update:modelValue', this.lValue) - } + return typeof this.modelValue !== 'undefined' }, - isCustom () { - return this.preset === 'custom' + localFontsList () { + return this.$store.state.interface.localFonts }, - preset: { - get () { - if (this.family === 'serif' || - this.family === 'sans-serif' || - this.family === 'monospace' || - this.family === 'inherit') { - return this.family - } else { - return 'custom' - } - }, - set (v) { - this.family = v === 'custom' ? '' : v - } + localFontsSize () { + return this.$store.state.interface.localFonts?.length } } } diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue @@ -1,6 +1,6 @@ <template> <div - class="font-control style-control" + class="font-control" :class="{ custom: isCustom }" > <label @@ -10,67 +10,137 @@ > {{ label }} </label> - <input + {{ ' ' }} + <Checkbox v-if="typeof fallback !== 'undefined'" :id="name + '-o'" - :aria-labelledby="name + '-label'" - class="opt exlcude-disabled visible-for-screenreader-only" - type="checkbox" - :checked="present" + :modelValue="present" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" > - <label - v-if="typeof fallback !== 'undefined'" - class="opt-l" - :for="name + '-o'" - :aria-hidden="true" - /> - {{ ' ' }} - <Select - :id="name + '-font-switcher'" - v-model="preset" - :disabled="!present" - class="font-switcher" - > - <option - v-for="option in availableOptions" - :key="option" - :value="option" + {{ $t('settings.style.themes3.define') }} + </Checkbox> + <p v-if="modelValue?.family"> + <label + v-if="manualEntry" + :id="name + '-label'" + :for="preset === 'custom' ? name : name + '-font-switcher'" + class="label" > - {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} - </option> - </Select> - <input - v-if="isCustom" - :id="name" - v-model="family" - class="custom-font" - type="text" - > + <i18n-t + keypath="settings.style.themes3.font.entry" + tag="span" + > + <template #fontFamily> + <code>font-family</code> + </template> + </i18n-t> + </label> + <label + v-else + :id="name + '-label'" + :for="preset === 'custom' ? name : name + '-font-switcher'" + class="label" + > + {{ $t('settings.style.themes3.font.select') }} + </label> + {{ ' ' }} + <span + v-if="manualEntry" + class="btn-group" + > + <button + class="btn button-default" + @click="toggleManualEntry" + :title="$t('settings.style.themes3.font.lookup_local_fonts')" + > + <FAIcon + fixed-width + icon="font" + /> + </button> + <input + :id="name" + :model-value="modelValue.family" + class="input custom-font" + type="text" + @update:modelValue="$emit('update:modelValue', { ...(modelValue || {}), family: $event.target.value })" + > + </span> + <span + v-else + class="btn-group" + > + <button + class="btn button-default" + @click="toggleManualEntry" + :title="$t('settings.style.themes3.font.enter_manually')" + > + <FAIcon + fixed-width + icon="keyboard" + /> + </button> + <Select + :id="name + '-local-font-switcher'" + :model-value="modelValue?.family" + class="custom-font" + @update:modelValue="v => $emit('update:modelValue', { ...(modelValue || {}), family: v })" + > + <optgroup + :label="$t('settings.style.themes3.font.group-builtin')" + > + <option + v-for="option in availableOptions" + :key="option" + :value="option" + :style="{ fontFamily: option === 'inherit' ? null : option }" + > + {{ $t('settings.style.themes3.font.builtin.' + option) }} + </option> + </optgroup> + <optgroup + v-if="localFontsSize > 0" + :label="$t('settings.style.themes3.font.group-local')" + > + <option + v-for="option in localFontsList" + :key="option" + :value="option" + :style="{ fontFamily: option }" + > + {{ option }} + </option> + </optgroup> + <optgroup + v-else + :label="$t('settings.style.themes3.font.group-local')" + > + <option disabled> + {{ $t('settings.style.themes3.font.local-unavailable1') }} + </option> + <option disabled> + {{ $t('settings.style.themes3.font.local-unavailable2') }} + </option> + </optgroup> + </Select> + </span> + </p> </div> </template> <script src="./font_control.js"></script> <style lang="scss"> -@import "../../variables"; - .font-control { - input.custom-font { - min-width: 10em; + .custom-font { + min-width: 20em; + max-width: 20em; } +} - &.custom { - /* TODO Should make proper joiners... */ - .font-switcher { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - .custom-font { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } +.invalid-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; } </style> diff --git a/src/components/fun_text.style.js b/src/components/fun_text.style.js @@ -0,0 +1,40 @@ +export default { + name: 'FunText', + selector: '/*fun-text*/', + virtual: true, + variants: { + greentext: '.greentext', + cyantext: '.cyantext' + }, + states: { + faint: '.faint' + }, + defaultRules: [ + { + directives: { + textColor: '--text', + textAuto: 'preserve' + } + }, + { + state: ['faint'], + directives: { + textOpacity: 0.5 + } + }, + { + variant: 'greentext', + directives: { + textColor: '--cGreen', + textAuto: 'preserve' + } + }, + { + variant: 'cyantext', + directives: { + textColor: '--cBlue', + textAuto: 'preserve' + } + } + ] +} diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue @@ -87,8 +87,6 @@ <script src='./gallery.js'></script> <style lang="scss"> -@import "../../variables"; - .Gallery { .gallery-rows { display: flex; diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue @@ -4,7 +4,7 @@ v-for="(notice, index) in notices" :key="index" class="alert global-notice" - :class="{ ['global-' + notice.level]: true }" + :class="{ [notice.level]: true }" > <div class="notice-message"> {{ $t(notice.messageKey, notice.messageArgs) }} @@ -25,8 +25,6 @@ <script src="./global_notice_list.js"></script> <style lang="scss"> -@import "../../variables"; - .global-notice-list { position: fixed; top: calc(var(--navbar-height) + 0.5em); @@ -52,48 +50,8 @@ } } - .global-error { - background-color: var(--alertPopupError, $fallback--cRed); - color: var(--alertPopupErrorText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupErrorText, $fallback--text); - } - } - - .global-warning { - background-color: var(--alertPopupWarning, $fallback--cOrange); - color: var(--alertPopupWarningText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupWarningText, $fallback--text); - } - } - - .global-success { - background-color: var(--alertPopupSuccess, $fallback--cGreen); - color: var(--alertPopupSuccessText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupSuccessText, $fallback--text); - } - } - - .global-info { - background-color: var(--alertPopupNeutral, $fallback--fg); - color: var(--alertPopupNeutralText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupNeutralText, $fallback--text); - } - } - .close-notice { padding-right: 0.2em; - - .svg-inline--fa:hover { - opacity: 0.6; - } } } </style> diff --git a/src/components/icon.style.js b/src/components/icon.style.js @@ -0,0 +1,14 @@ +export default { + name: 'Icon', + virtual: true, + selector: '.svg-inline--fa', + defaultRules: [ + { + component: 'Icon', + directives: { + textColor: '$blend(--stack, 0.5, --parent--text)', + textAuto: 'no-auto' + } + } + ] +} diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue @@ -41,7 +41,7 @@ <input ref="input" type="file" - class="image-cropper-img-input" + class="input image-cropper-img-input" :accept="mimes" > </div> diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue @@ -3,6 +3,7 @@ <form> <input ref="input" + class="input" type="file" @change="change" > diff --git a/src/components/input.style.js b/src/components/input.style.js @@ -0,0 +1,60 @@ +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + +export default { + name: 'Input', + selector: '.input', + variant: { + checkbox: '.-checkbox', + radio: '.-radio' + }, + states: { + disabled: ':disabled', + hover: ':hover:not(:disabled)', + focused: ':focus-within' + }, + validInnerComponents: [ + 'Text' + ], + defaultRules: [ + { + component: 'Root', + directives: { + '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)' + } + }, + { + variant: 'checkbox', + directives: { + roundness: 1 + } + }, + { + directives: { + '--font': 'generic | inherit', + background: '--fg, -5', + roundness: 3, + shadow: [{ + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 + }, '--defaultInputBevel'] + } + }, + { + state: ['hover'], + directives: { + shadow: [hoverGlow, '--defaultInputBevel'] + } + } + ] +} diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js @@ -3,6 +3,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' const tabModeDict = { mentions: ['mention'], + statuses: ['status'], 'likes+repeats': ['repeat', 'like'], follows: ['follow'], reactions: ['pleroma:emoji_reaction'], diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue @@ -10,10 +10,14 @@ :on-switch="onModeSwitch" > <span - key="mentions" + key="statuses" :label="$t('nav.mentions')" /> <span + key="statuses" + :label="$t('interactions.statuses')" + /> + <span key="likes+repeats" :label="$t('interactions.favs_repeats')" /> @@ -39,6 +43,7 @@ <Notifications ref="notifications" :no-heading="true" + :no-extra="true" :minimal-mode="true" :filter-mode="filterMode" /> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -104,8 +104,6 @@ export default { </script> <style lang="scss"> -@import "../../variables"; - .interface-language-switcher { .language-select { margin-right: 1em; diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue @@ -33,8 +33,6 @@ <script src="./link-preview.js"></script> <style lang="scss"> -@import "../../variables"; - .link-preview-card { display: flex; flex-direction: row; @@ -51,8 +49,7 @@ width: 100%; height: 100%; object-fit: cover; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border-radius: var(--roundness); } } @@ -82,13 +79,10 @@ margin: 2em 0; } - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); border-style: solid; border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-radius: var(--roundness); + border-color: var(--border); } </style> diff --git a/src/components/link.style.js b/src/components/link.style.js @@ -0,0 +1,24 @@ +export default { + name: 'Link', + selector: 'a', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + component: 'Link', + directives: { + textColor: '--link' + } + }, + { + component: 'Link', + state: ['faint'], + directives: { + textOpacity: 0.5, + textOpacityMode: 'fake' + } + } + ] +} diff --git a/src/components/list/list.vue b/src/components/list/list.vue @@ -7,6 +7,7 @@ v-for="item in items" :key="getKey(item)" class="list-item" + :class="[getClass(item), nonInteractive ? '-non-interactive' : '']" role="listitem" > <slot @@ -33,24 +34,15 @@ export default { getKey: { type: Function, default: item => item.id + }, + getClass: { + type: Function, + default: item => '' + }, + nonInteractive: { + type: Boolean, + default: false } } } </script> - -<style lang="scss"> -@import "../../variables"; - -.list { - &-item:not(:last-child) { - border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); - } - - &-empty-content { - text-align: center; - padding: 10px; - } -} -</style> diff --git a/src/components/list/list_item.style.js b/src/components/list/list_item.style.js @@ -0,0 +1,48 @@ +export default { + name: 'ListItem', + selector: '.list-item', + states: { + active: '.-active', + hover: ':hover:not(.-non-interactive)' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar' + ], + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0 + } + }, + { + state: ['active'], + directives: { + background: '--inheritedBackground, 10', + opacity: 1 + } + }, + { + state: ['hover'], + directives: { + background: '--inheritedBackground, 10', + opacity: 1 + } + }, + { + state: ['hover', 'active'], + directives: { + background: '--inheritedBackground, 20', + opacity: 1 + } + } + ] +} diff --git a/src/components/lists_card/lists_card.vue b/src/components/lists_card/lists_card.vue @@ -21,8 +21,6 @@ <script src="./lists_card.js"></script> <style lang="scss"> -@import "../../variables"; - .list-card { display: flex; } @@ -35,18 +33,6 @@ .button-list-edit { margin: 0; padding: 1em; - color: $fallback--link; - color: var(--link, $fallback--link); - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - } + color: var(--link); } </style> diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue @@ -36,6 +36,7 @@ id="list-edit-title" ref="title" v-model="titleDraft" + class="input" > <button v-if="id" @@ -164,8 +165,6 @@ <script src="./lists_edit.js"></script> <style lang="scss"> -@import "../../variables"; - .ListEdit { --panel-body-padding: 0.5em; diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue @@ -10,6 +10,7 @@ <input ref="search" v-model="query" + class="input" :placeholder="$t('lists.search')" @input="onInput" > @@ -27,8 +28,6 @@ <script src="./lists_user_search.js"></script> <style lang="scss"> -@import "../../variables"; - .ListsUserSearch { .input-wrap { display: flex; diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue @@ -18,7 +18,7 @@ id="username" v-model="user.username" :disabled="loggingIn" - class="form-control" + class="input form-control" :placeholder="$t('login.placeholder')" > </div> @@ -29,7 +29,7 @@ ref="passwordInput" v-model="user.password" :disabled="loggingIn" - class="form-control" + class="input form-control" type="password" > </div> @@ -93,8 +93,6 @@ <script src="./login_form.js"></script> <style lang="scss"> -@import "../../variables"; - .login-form { display: flex; flex-direction: column; diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue @@ -36,8 +36,6 @@ <script src="./media_upload.js"></script> <style lang="scss"> -@import "../../variables"; - .media-upload { .hidden-input-file { display: none; diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss @@ -1,10 +1,7 @@ -@import "../../variables"; - .MentionLink { position: relative; white-space: normal; display: inline; - color: var(--link); word-break: normal; & .new, @@ -14,7 +11,7 @@ } .mention-avatar { - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); width: 1.5em; height: 1.5em; vertical-align: middle; @@ -61,8 +58,10 @@ } &.-has-selection { - color: var(--alertNeutralText, $fallback--text); - background-color: var(--alertNeutral, $fallback--fg); + --color: var(--selectionText); + --link: var(--selectionText); + + background-color: var(--selectionBackground); } .at { @@ -102,7 +101,7 @@ } .serverName.-faded { - color: var(--faintLink, $fallback--link); + color: var(--linkFaint); } } diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue @@ -22,7 +22,7 @@ :class="classnames" > <a - class="short button-unstyled" + class="short" :class="{ '-with-tooltip': shouldShowTooltip }" :href="url" @click.prevent="onClick" diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue @@ -22,13 +22,13 @@ /> </span><button v-if="!expanded" - class="button-unstyled showMoreLess" + class="button-unstyled -link showMoreLess" @click="toggleShowMore" > {{ $t('status.plus_more', { number: extraMentions.length }) }} </button><button v-if="expanded" - class="button-unstyled showMoreLess" + class="button-unstyled -link showMoreLess" @click="toggleShowMore" > {{ $t('general.show_less') }} diff --git a/src/components/menu_item.style.js b/src/components/menu_item.style.js @@ -0,0 +1,90 @@ +export default { + name: 'MenuItem', + selector: '.menu-item', + validInnerComponents: [ + 'Text', + 'Icon', + 'Input', + 'Border', + 'ButtonUnstyled', + 'Badge', + 'Avatar' + ], + states: { + hover: ':hover', + active: '.-active' + }, + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0 + } + }, + { + state: ['hover'], + directives: { + background: '$mod(--bg, 5)', + opacity: 1 + } + }, + { + state: ['active'], + directives: { + background: '$mod(--bg, 10)', + opacity: 1 + } + }, + { + state: ['active', 'hover'], + directives: { + background: '$mod(--bg, 15)', + opacity: 1 + } + }, + { + component: 'Text', + parent: { + component: 'MenuItem', + state: ['hover'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Text', + parent: { + component: 'MenuItem', + state: ['active'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Icon', + parent: { + component: 'MenuItem', + state: ['active'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Icon', + parent: { + component: 'MenuItem', + state: ['hover'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + } + ] +} diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue @@ -16,7 +16,7 @@ <input id="code" v-model="code" - class="form-control" + class="input form-control" > </div> diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue @@ -18,7 +18,7 @@ <input id="code" v-model="code" - class="form-control" + class="input form-control" > </div> diff --git a/src/components/mobile_drawer.style.js b/src/components/mobile_drawer.style.js @@ -0,0 +1,41 @@ +export default { + name: 'MobileDrawer', + selector: '.mobile-drawer', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'PanelHeader', + 'MenuItem', + 'Notification', + 'Alert', + 'UserCard' + ], + defaultRules: [ + { + directives: { + background: '--bg', + backgroundNoCssColor: 'yes' + } + }, + { + component: 'PanelHeader', + parent: { component: 'MobileDrawer' }, + directives: { + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js @@ -1,7 +1,10 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue' -import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' +import { + unseenNotificationsFromStore, + countExtraNotifications +} from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' import NavigationPins from 'src/components/navigation/navigation_pins.vue' import { mapGetters } from 'vuex' @@ -11,7 +14,8 @@ import { faBell, faBars, faArrowUp, - faMinus + faMinus, + faCheckDouble } from '@fortawesome/free-solid-svg-icons' library.add( @@ -19,7 +23,8 @@ library.add( faBell, faBars, faArrowUp, - faMinus + faMinus, + faCheckDouble ) const MobileNav = { @@ -50,8 +55,14 @@ const MobileNav = { return unseenNotificationsFromStore(this.$store) }, unseenNotificationsCount () { + return this.unseenNotifications.length + countExtraNotifications(this.$store) + }, + unseenCount () { return this.unseenNotifications.length }, + unseenCountBadgeText () { + return `${this.unseenCount ? this.unseenCount : ''}` + }, hideSitename () { return this.$store.state.instance.hideSitename }, sitename () { return this.$store.state.instance.name }, isChat () { @@ -64,6 +75,9 @@ const MobileNav = { shouldConfirmLogout () { return this.$store.getters.mergedConfig.modalOnLogout }, + closingDrawerMarksAsSeen () { + return this.$store.getters.mergedConfig.closingDrawerMarksAsSeen + }, ...mapGetters(['unreadChatCount']) }, methods: { @@ -78,7 +92,7 @@ const MobileNav = { // make sure to mark notifs seen only when the notifs were open and not // from close-calls. this.notificationsOpen = false - if (markRead) { + if (markRead && this.closingDrawerMarksAsSeen) { this.markNotificationsAsSeen() } } @@ -114,7 +128,6 @@ const MobileNav = { this.hideConfirmLogout() }, markNotificationsAsSeen () { - // this.$refs.notifications.markAsSeen() this.$store.dispatch('markNotificationsAsSeen') }, onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue @@ -20,7 +20,7 @@ /> <div v-if="(unreadChatCount && !chatsPinned) || unreadAnnouncementCount" - class="alert-dot" + class="badge -dot -notification" /> </button> <NavigationPins class="pins" /> @@ -37,20 +37,26 @@ /> <div v-if="unseenNotificationsCount" - class="alert-dot" + class="badge -dot -notification" /> </button> </div> </nav> <aside v-if="currentUser" - class="mobile-notifications-drawer" + class="mobile-notifications-drawer mobile-drawer" :class="{ '-closed': !notificationsOpen }" @touchstart.stop="notificationsTouchStart" @touchmove.stop="notificationsTouchMove" > - <div class="mobile-notifications-header"> - <span class="title">{{ $t('notifications.notifications') }}</span> + <div class="panel-heading mobile-notifications-header"> + <span class="title"> + {{ $t('notifications.notifications') }} + <span + v-if="unseenCountBadgeText" + class="badge -notification unseen-count" + >{{ unseenCountBadgeText }}</span> + </span> <span class="spacer" /> <button v-if="notificationsAtTop" @@ -67,6 +73,17 @@ </FALayers> </button> <button + v-if="!closingDrawerMarksAsSeen" + class="button-unstyled mobile-nav-button" + :title="$t('nav.mobile_notifications_mark_as_seen')" + @click.stop.prevent="markNotificationsAsSeen()" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="check-double" + /> + </button> + <button class="button-unstyled mobile-nav-button" :title="$t('nav.mobile_notifications_close')" @click.stop.prevent="closeMobileNotifications(true)" @@ -106,21 +123,19 @@ <script src="./mobile_nav.js"></script> <style lang="scss"> -@import "../../variables"; - .MobileNav { z-index: var(--ZI_navbar); .mobile-nav { display: grid; line-height: var(--navbar-height); - grid-template-rows: 50px; + grid-template-rows: var(--navbar-height); grid-template-columns: 2fr auto; width: 100%; box-sizing: border-box; a { - color: var(--topBarLink, $fallback--link); + color: var(--link); } } @@ -148,19 +163,6 @@ display: flex; } - .alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - } - .mobile-notifications-drawer { width: 100%; height: 100vh; @@ -168,13 +170,13 @@ position: fixed; top: 0; left: 0; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); transition-property: transform; transition-duration: 0.25s; transform: translateX(0); z-index: var(--ZI_navbar); -webkit-overflow-scrolling: touch; + background: var(--background); &.-closed { transform: translateX(100%); @@ -188,14 +190,10 @@ justify-content: space-between; z-index: calc(var(--ZI_navbar) + 100); width: 100%; - height: 50px; - line-height: 50px; + height: 3.5em; + line-height: 3.5em; position: absolute; - color: var(--topBarText); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - box-shadow: 0 0 4px rgb(0 0 0 / 60%); - box-shadow: var(--topBarShadow); + box-shadow: var(--shadow); .spacer { flex: 1; @@ -216,15 +214,11 @@ } .mobile-notifications { - margin-top: 50px; + margin-top: 3.5em; width: 100vw; height: calc(100vh - var(--navbar-height)); overflow-x: hidden; overflow-y: scroll; - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); .notifications { padding: 0; diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -13,8 +13,6 @@ <script src="./mobile_post_status_button.js"></script> <style lang="scss"> -@import "../../variables"; - .MobilePostButton { &.button-default { width: 5em; @@ -25,8 +23,6 @@ right: 1.5em; // TODO: this needs its own color, it has to stand out enough and link color // is not very optimal for this particular use. - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); display: flex; justify-content: center; align-items: center; @@ -42,8 +38,7 @@ svg { font-size: 1.5em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } diff --git a/src/components/modal/modals.style.js b/src/components/modal/modals.style.js @@ -0,0 +1,9 @@ +export default { + name: 'Modals', + selector: '.modal-view', + lazy: true, + validInnerComponents: [ + 'Panel' + ], + defaultRules: [] +} diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue @@ -12,13 +12,13 @@ <div class="dropdown-menu"> <span v-if="canGrantRole"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleRight(&quot;admin&quot;)" > {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleRight(&quot;moderator&quot;)" > {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }} @@ -31,14 +31,14 @@ </span> <button v-if="canChangeActivationState" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleActivationStatus()" > {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }} </button> <button v-if="canDeleteAccount" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="deleteUserDialog(true)" > {{ $t('user_card.admin_menu.delete_account') }} @@ -50,74 +50,74 @@ /> <span v-if="canUseTagPolicy"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.FORCE_NSFW)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" /> {{ $t('user_card.admin_menu.force_nsfw') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.STRIP_MEDIA)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }" /> {{ $t('user_card.admin_menu.strip_media') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.FORCE_UNLISTED)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" /> {{ $t('user_card.admin_menu.force_unlisted') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.SANDBOX)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" /> {{ $t('user_card.admin_menu.sandbox') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" /> {{ $t('user_card.admin_menu.disable_remote_subscription') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" /> {{ $t('user_card.admin_menu.disable_any_subscription') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.QUARANTINE)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" /> {{ $t('user_card.admin_menu.quarantine') }} @@ -166,8 +166,6 @@ <script src="./moderation_tools.js"></script> <style lang="scss"> -@import "../../variables"; - .moderation-tools-popover { height: 100%; diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -227,6 +227,5 @@ <script src="./mrf_transparency_panel.js"></script> <style lang="scss"> -@import "../../variables"; @import "./mrf_transparency_panel"; </style> diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue @@ -37,7 +37,8 @@ </NavigationEntry> <div v-show="showTimelines" - class="timelines-background" + class="timelines-background menu-item-collapsible" + :class="{ '-expanded': showTimelines }" > <div class="timelines"> <NavigationEntry @@ -57,12 +58,11 @@ > <router-link :title="$t('lists.manage_lists')" - class="extra-button" + class="button-unstyled extra-button" :to="{ name: 'lists' }" @click.stop > <FAIcon - class="extra-button" fixed-width icon="wrench" /> @@ -75,7 +75,8 @@ </NavigationEntry> <div v-show="showLists" - class="timelines-background" + class="timelines-background menu-item-collapsible" + :class="{ '-expanded': showLists }" > <ListsMenuContent :show-pin="editMode || forceEditMode" @@ -102,12 +103,10 @@ <script src="./nav_panel.js"></script> <style lang="scss"> -@import "../../variables"; - .NavPanel { .panel { overflow: hidden; - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); } ul { @@ -116,33 +115,6 @@ padding: 0; } - li { - position: relative; - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - > li { - &:first-child .menu-item { - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); - } - - &:last-child .menu-item { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); - } - } - - li:last-child { - border: none; - } - .navigation-chevron { margin-left: 0.8em; margin-right: 0.8em; @@ -156,16 +128,6 @@ .timelines-background { padding: 0 0 0 0.6em; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - .timelines { - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); } .nav-panel-heading { diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue @@ -1,7 +1,6 @@ <template> <OptionalRouterLink v-slot="{ isActive, href, navigate } = {}" - ass="ass" :to="routeTo" > <li @@ -11,7 +10,7 @@ > <component :is="routeTo ? 'a' : 'button'" - class="main-link button-unstyled" + class="main-link" :href="href" @click="navigate" > @@ -35,7 +34,7 @@ <slot /> <div v-if="item.badgeGetter && getters[item.badgeGetter]" - class="badge badge-notification" + class="badge -notification" > {{ getters[item.badgeGetter] }} </div> @@ -63,73 +62,53 @@ <script src="./navigation_entry.js"></script> <style lang="scss"> -@import "../../variables"; +.NavigationEntry.menu-item { + --__line-height: 2.5em; + --__horizontal-gap: 0.5em; + --__vertical-gap: 0.4em; -.NavigationEntry { + padding: 0; display: flex; - box-sizing: border-box; align-items: baseline; - height: 3.5em; - line-height: 3.5em; - padding: 0 1em; - width: 100%; - color: $fallback--link; - color: var(--link, $fallback--link); - .timelines-chevron { - margin-right: 0; + &[aria-expanded] { + padding-right: var(--__horizontal-gap); } .main-link { + line-height: var(--__line-height); + box-sizing: border-box; flex: 1; + padding: var(--__vertical-gap) var(--__horizontal-gap); } .menu-icon { - margin-right: 0.8em; + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); + margin-right: var(--__horizontal-gap); + } + + .timelines-chevron { + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); + margin-right: 0; } .extra-button { - width: 3em; + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); text-align: center; &:last-child { - margin-right: -0.8em; - } - } - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - - .menu-icon { - --icon: var(--text, $fallback--icon); + margin-right: calc(-1 * var(--__horizontal-gap)); } } - &.-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - - .menu-icon { - --icon: var(--text, $fallback--icon); - } - - &:hover { - text-decoration: underline; - } + .badge { + margin: 0 var(--__horizontal-gap); } } </style> diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue @@ -3,7 +3,8 @@ <router-link v-for="item in pinnedList" :key="item.name" - class="pinned-item" + class="button-unstyled pinned-item" + active-class="toggled" :to="getRouteTo(item)" :title="item.labelRaw || $t(item.label)" > @@ -18,7 +19,7 @@ >{{ item.iconLetter }}</span> <div v-if="item.badgeGetter && getters[item.badgeGetter]" - class="alert-dot" + class="badge -dot -notification" /> </router-link> </span> @@ -27,25 +28,12 @@ <script src="./navigation_pins.js"></script> <style lang="scss"> -@import "../../variables"; - .NavigationPins { display: flex; flex-wrap: wrap; overflow: hidden; height: 100%; - .alert-dot { - border-radius: 100%; - height: 0.5em; - width: 0.5em; - position: absolute; - right: calc(50% - 0.75em); - top: calc(50% - 0.5em); - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - } - .pinned-item { position: relative; flex: 1 0 3em; @@ -60,15 +48,9 @@ margin: 0; } - &.router-link-active { - color: $fallback--text; - color: var(--panelText, $fallback--text); + &.toggled { + margin-bottom: -4px; border-bottom: 4px solid; - - & .svg-inline--fa, - & .iconLetter { - color: inherit; - } } } } diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -50,6 +50,7 @@ const Notification = { } }, props: ['notification'], + emits: ['interacted'], components: { StatusContent, UserAvatar, @@ -72,6 +73,9 @@ const Notification = { getUser (notification) { return this.$store.state.users.usersObject[notification.from_profile.id] }, + interacted () { + this.$emit('interacted') + }, toggleMute () { this.unmuted = !this.unmuted }, @@ -95,6 +99,7 @@ const Notification = { } }, doApprove () { + this.$emit('interacted') this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) @@ -114,6 +119,7 @@ const Notification = { } }, doDeny () { + this.$emit('interacted') this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) .then(() => { this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss @@ -1,13 +1,15 @@ -@import "../../variables"; - // TODO Copypaste from Status, should unify it somehow .Notification { border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); word-wrap: break-word; word-break: break-word; + &.Status { + /* stylelint-disable-next-line declaration-no-important */ + background-color: transparent !important; + } + --emoji-size: 14px; &:hover { @@ -71,28 +73,22 @@ } &.-type--repeat .type-icon { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } &.-type--follow .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } &.-type--follow-request .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } &.-type--like .type-icon { - color: orange; - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } &.-type--move .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } } diff --git a/src/components/notification/notification.style.js b/src/components/notification/notification.style.js @@ -0,0 +1,18 @@ +export default { + name: 'Notification', + selector: '.Notification', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar', + 'Attachment', + 'PollGraph' + ], + defaultRules: [] +} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -1,11 +1,12 @@ <template> <article - v-if="notification.type === 'mention'" + v-if="notification.type === 'mention' || notification.type === 'status'" > <Status class="Notification" :compact="true" :statusoid="notification.status" + @interacted="interacted" /> </article> <article v-else> @@ -154,7 +155,7 @@ <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" - class="timeago-link faint-link" + class="timeago-link faint" > <Timeago :time="notification.created_at" @@ -246,9 +247,8 @@ /> <template v-else> <StatusContent - :class="{ faint: !statusExpanded }" :compact="!statusExpanded" - :status="notification.action" + :status="notification.status" /> </template> </div> diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue @@ -8,65 +8,74 @@ <template #content> <div class="dropdown-menu"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('likes')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.likes }" />{{ $t('settings.notification_visibility_likes') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('repeats')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.repeats }" />{{ $t('settings.notification_visibility_repeats') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('follows')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.follows }" />{{ $t('settings.notification_visibility_follows') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('mentions')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.mentions }" />{{ $t('settings.notification_visibility_mentions') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" + @click="toggleNotificationFilter('statuses')" + > + <span + class="input menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.statuses }" + />{{ $t('settings.notification_visibility_statuses') }} + </button> + <button + class="menu-item dropdown-item" @click="toggleNotificationFilter('emojiReactions')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.emojiReactions }" />{{ $t('settings.notification_visibility_emoji_reactions') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('moves')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.moves }" />{{ $t('settings.notification_visibility_moves') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('polls')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.polls }" />{{ $t('settings.notification_visibility_polls') }} </button> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js @@ -1,12 +1,15 @@ import { computed } from 'vue' import { mapGetters } from 'vuex' import Notification from '../notification/notification.vue' +import ExtraNotifications from '../extra_notifications/extra_notifications.vue' import NotificationFilters from './notification_filters.vue' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import { notificationsFromStore, filteredNotificationsFromStore, - unseenNotificationsFromStore + unseenNotificationsFromStore, + countExtraNotifications, + ACTIONABLE_NOTIFICATION_TYPES } from '../../services/notification_utils/notification_utils.js' import FaviconService from '../../services/favicon_service/favicon_service.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -23,14 +26,20 @@ const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const Notifications = { components: { Notification, - NotificationFilters + NotificationFilters, + ExtraNotifications }, props: { // Disables panel styles, unread mark, potentially other notification-related actions // meant for "Interactions" timeline minimalMode: Boolean, - // Custom filter mode, an array of strings, possible values 'mention', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline + // Custom filter mode, an array of strings, possible values 'mention', 'status', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline filterMode: Array, + // Do not show extra notifications + noExtra: { + type: Boolean, + default: false + }, // Disable teleporting (i.e. for /users/user/notifications) disableTeleport: Boolean }, @@ -57,22 +66,36 @@ const Notifications = { return notificationsFromStore(this.$store) }, error () { - return this.$store.state.statuses.notifications.error + return this.$store.state.notifications.error }, unseenNotifications () { return unseenNotificationsFromStore(this.$store) }, filteredNotifications () { - return filteredNotificationsFromStore(this.$store, this.filterMode) + if (this.unseenAtTop) { + return [ + ...filteredNotificationsFromStore(this.$store).filter(n => this.shouldShowUnseen(n)), + ...filteredNotificationsFromStore(this.$store).filter(n => !this.shouldShowUnseen(n)) + ] + } else { + return filteredNotificationsFromStore(this.$store, this.filterMode) + } + }, + unseenCountBadgeText () { + return `${this.unseenCount ? this.unseenCount : ''}${this.extraNotificationsCount ? '*' : ''}` }, unseenCount () { return this.unseenNotifications.length }, + ignoreInactionableSeen () { return this.$store.getters.mergedConfig.ignoreInactionableSeen }, + extraNotificationsCount () { + return countExtraNotifications(this.$store) + }, unseenCountTitle () { - return this.unseenCount + (this.unreadChatCount) + this.unreadAnnouncementCount + return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount }, loading () { - return this.$store.state.statuses.notifications.loading + return this.$store.state.notifications.loading }, noHeading () { const { layoutType } = this.$store.state.interface @@ -94,6 +117,10 @@ const Notifications = { return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) }, noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, + unseenAtTop () { return this.$store.getters.mergedConfig.unseenAtTop }, + showExtraNotifications () { + return !this.noExtra + }, ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) }, mounted () { @@ -137,11 +164,28 @@ const Notifications = { scrollToTop () { const scrollable = this.scrollerRef scrollable.scrollTo({ top: this.$refs.root.offsetTop }) - // this.$refs.root.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, updateScrollPosition () { this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop }, + shouldShowUnseen (notification) { + if (notification.seen) return false + + const actionable = ACTIONABLE_NOTIFICATION_TYPES.has(notification.type) + return this.ignoreInactionableSeen ? actionable : true + }, + /* "Interacted" really refers to "actionable" notifications that require user input, + * everything else (likes/repeats/reacts) cannot be acted and therefore we just clear + * the "seen" status upon any clicks on them + */ + notificationClicked (notification) { + const { id } = notification + this.$store.dispatch('notificationClicked', { id }) + }, + notificationInteracted (notification) { + const { id } = notification + this.$store.dispatch('markSingleNotificationAsSeen', { id }) + }, markAsSeen () { this.$store.dispatch('markNotificationsAsSeen') this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Notifications { &:not(.minimal) { // a bit of a hack to allow scrolling below notifications @@ -7,8 +5,7 @@ } .loadmore-error { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } .notification { @@ -25,7 +22,7 @@ &.unseen { .notification-overlay { - background-image: linear-gradient(135deg, var(--badgeNotification, $fallback--cRed) 4px, transparent 10px); + background-image: linear-gradient(135deg, var(--badgeNotification) 4px, transparent 10px); } } } @@ -35,6 +32,11 @@ .notification { box-sizing: border-box; + /* TODO cleanup this */ + .Status { + flex: 1; + } + &:hover .animated.Avatar { canvas { display: none; @@ -60,24 +62,17 @@ width: 32px; height: 32px; } - - .faint { - --link: var(--faintLink); - --text: var(--faint); - } } .follow-request-accept { &:hover { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } .follow-request-reject { &:hover { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); + color: var(--cRed); } } @@ -97,11 +92,6 @@ } } - /* TODO cleanup this */ - .Status { - flex: 1; - } - time { white-space: nowrap; } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue @@ -17,9 +17,9 @@ <div class="title"> {{ $t('notifications.notifications') }} <span - v-if="unseenCount" - class="badge badge-notification unseen-count" - >{{ unseenCount }}</span> + v-if="unseenCountBadgeText" + class="badge -notification unseen-count" + >{{ unseenCountBadgeText }}</span> </div> <div v-if="showScrollTop" @@ -55,14 +55,25 @@ role="feed" > <div + v-if="showExtraNotifications" + role="listitem" + class="notification" + > + <extra-notifications /> + </div> + <div v-for="notification in notificationsToDisplay" :key="notification.id" role="listitem" class="notification" - :class="{unseen: !minimalMode && !notification.seen}" + :class="{unseen: !minimalMode && shouldShowUnseen(notification)}" + @click="e => notificationClicked(notification)" > <div class="notification-overlay" /> - <notification :notification="notification" /> + <notification + :notification="notification" + @interacted="e => notificationInteracted(notification)" + /> </div> </div> <div class="panel-footer"> @@ -74,7 +85,7 @@ </div> <button v-else-if="!loading" - class="button-unstyled -link -fullwidth" + class="button-unstyled -link text-center" @click.prevent="fetchOlderNotifications()" > <div class="new-status-notification text-center"> diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue @@ -18,7 +18,7 @@ /> <input :id="name" - class="input-number" + class="input input-number" type="number" :value="modelValue || fallback" :disabled="!present || disabled" diff --git a/src/components/panel.style.js b/src/components/panel.style.js @@ -0,0 +1,51 @@ +export default { + name: 'Panel', + selector: '.panel', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'PanelHeader', + 'MenuItem', + 'Post', + 'Notification', + 'Alert', + 'UserCard', + 'Chat', + 'Attachment', + 'Tab', + 'ListItem' + ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'Input', + 'PanelHeader', + 'Alert' + ], + defaultRules: [ + { + directives: { + backgroundNoCssColor: 'yes', + background: '--bg', + roundness: 3, + blur: '5px', + shadow: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js @@ -0,0 +1,24 @@ +export default { + name: 'PanelHeader', + selector: '.panel-heading', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Badge', + 'Alert', + 'Avatar' + ], + defaultRules: [ + { + component: 'PanelHeader', + directives: { + backgroundNoCssColor: 'yes', + background: '--fg', + shadow: [] + } + } + ] +} diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue @@ -23,22 +23,18 @@ export default {} </script> <style lang="scss"> -@import "src/variables"; - .panel-loading { display: flex; height: 100%; align-items: center; justify-content: center; font-size: 2em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); .loading-text svg { line-height: 0; vertical-align: middle; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } </style> diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue @@ -30,7 +30,7 @@ <div v-else> <p v-if="passwordResetRequested" - class="password-reset-required error" + class="alert password-reset-required error" > {{ $t('password_reset.password_reset_required') }} </p> @@ -43,7 +43,7 @@ v-model="user.email" :disabled="isPending" :placeholder="$t('password_reset.placeholder')" - class="form-control" + class="input form-control" type="input" > </div> @@ -77,8 +77,6 @@ <script src="./password_reset.js"></script> <style lang="scss"> -@import "../../variables"; - .password-reset-form { display: flex; flex-direction: column; @@ -117,11 +115,6 @@ margin: 0.3em 0 1em; } - .password-reset-required { - background-color: var(--alertError, $fallback--alertError); - padding: 10px 0; - } - .notice-dismissible { padding-right: 2rem; } diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js @@ -38,7 +38,7 @@ export default { return (this.poll && this.poll.options) || [] }, expiresAt () { - return (this.poll && this.poll.expires_at) || 0 + return (this.poll && this.poll.expires_at) || null }, expired () { return (this.poll && this.poll.expired) || false diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue @@ -37,12 +37,14 @@ :role="poll.multiple ? 'checkbox' : 'radio'" :aria-labelledby="`option-vote-${randomSeed}-${index}`" :aria-checked="choices[index]" + class="input unstyled" @click="activateOption(index)" > + <!-- TODO: USE CHECKBOX --> <input v-if="poll.multiple" type="checkbox" - class="poll-checkbox" + class="input -checkbox poll-checkbox" :disabled="loading" :value="index" > @@ -51,6 +53,7 @@ type="radio" :disabled="loading" :value="index" + class="input -radio" > <label class="option-vote"> <RichContent @@ -75,13 +78,16 @@ </button> <div class="total"> <template v-if="typeof poll.voters_count === 'number'"> - {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}&nbsp;·&nbsp; + {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }} </template> <template v-else> - {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp; + {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} </template> + <span v-if="expiresAt !== null"> + &nbsp;·&nbsp; + </span> </div> - <span> + <span v-if="expiresAt !== null"> <i18n-t scope="global" :keypath="expired ? 'polls.expired' : 'polls.expires_in'" @@ -100,8 +106,6 @@ <script src="./poll.js"></script> <style lang="scss"> -@import "../../variables"; - .poll { .votes { display: flex; @@ -111,6 +115,10 @@ .poll-option { margin: 0.75em 0.5em; + + .input { + line-height: inherit; + } } .option-result { @@ -118,8 +126,7 @@ display: flex; flex-direction: row; position: relative; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--textLight); } .option-result-label { @@ -138,12 +145,7 @@ .result-fill { height: 100%; position: absolute; - color: $fallback--text; - color: var(--pollText, $fallback--text); - background-color: $fallback--lightBg; - background-color: var(--poll, $fallback--lightBg); - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); top: 0; left: 0; transition: width 0.5s; diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue @@ -13,7 +13,7 @@ :id="`poll-${index}`" v-model="options[index]" size="1" - class="poll-option-input" + class="input poll-option-input" type="text" :placeholder="$t('polls.option')" :maxlength="maxLength" @@ -67,7 +67,7 @@ <input v-model="expiryAmount" type="number" - class="expiry-amount hide-number-spinner" + class="input expiry-amount hide-number-spinner" :min="minExpirationInCurrentUnit" :max="maxExpirationInCurrentUnit" @change="expiryAmountChange" @@ -95,8 +95,6 @@ <script src="./poll_form.js"></script> <style lang="scss"> -@import "../../variables"; - .poll-form { display: flex; flex-direction: column; diff --git a/src/components/poll/poll_graph.style.js b/src/components/poll/poll_graph.style.js @@ -0,0 +1,12 @@ +export default { + name: 'PollGraph', + selector: '.result-fill', + defaultRules: [ + { + directives: { + background: '--accent', + opacity: 0.5 + } + } + ] +} diff --git a/src/components/popover.style.js b/src/components/popover.style.js @@ -0,0 +1,36 @@ +export default { + name: 'Popover', + selector: '.popover', + lazy: true, + variants: { + modal: '.modal' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'MenuItem', + 'Post', + 'UserCard' + ], + defaultRules: [ + { + directives: { + background: '--bg', + blur: '10px', + shadow: [{ + x: 2, + y: 2, + blur: 3, + spread: 0, + color: '#000000', + alpha: 0.5 + }] + } + } + ] +} diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue @@ -42,8 +42,6 @@ <script src="./popover.js" /> <style lang="scss"> -@import "../../variables"; - .popover-trigger-button { display: inline-block; } @@ -53,81 +51,54 @@ position: fixed; min-width: 0; max-width: calc(100vw - 20px); - box-shadow: 2px 2px 3px rgb(0 0 0 / 50%); - box-shadow: var(--popupShadow); + box-shadow: var(--shadow); } .popover-default { &::after { content: ""; position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 3; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + top: -1px; + bottom: -1px; + left: -1px; + right: -1px; + z-index: -1px; + box-shadow: var(--shadow); pointer-events: none; } - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--text; - color: var(--popoverText, $fallback--text); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --postLink: var(--popoverPostLink, $fallback--link); - --postFaintLink: var(--popoverPostFaintLink, $fallback--link); - --icon: var(--popoverIcon, $fallback--icon); + border-radius: var(--roundness); + border-color: var(--border); + border-style: solid; + border-width: 1px; + background-color: var(--background); } .dropdown-menu { display: block; - padding: 0.5rem 0; + padding: 0; font-size: 1em; text-align: left; list-style: none; max-width: 100vw; z-index: var(--ZI_popover_override, var(--ZI_popovers)); white-space: nowrap; + background-color: var(--background); .dropdown-divider { height: 0; margin: 0.5rem 0; overflow: hidden; - border-top: 1px solid $fallback--border; - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } .dropdown-item { - line-height: 21px; - overflow: hidden; - display: block; - padding: 0.5em 0.75em; - clear: both; - font-weight: 400; - text-align: inherit; - white-space: nowrap; border: none; - border-radius: 0; - background-color: transparent; - box-shadow: none; - width: 100%; - height: 100%; - box-sizing: border-box; - - --btnText: var(--popoverText, $fallback--text); &-icon { svg { - width: 22px; - margin-right: 0.75rem; - color: var(--menuPopoverIcon, $fallback--icon); + width: var(--__line-height); + margin-right: var(--__horizontal-gap); } } @@ -138,40 +109,18 @@ } } - &:active, - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - box-shadow: none; - - --btnText: var(--selectedMenuPopoverText, $fallback--link); - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - - svg { - color: var(--selectedMenuPopoverIcon, $fallback--icon); - - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } - } - .menu-checkbox { display: inline-block; vertical-align: middle; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - line-height: 22px; + min-width: calc(var(--__line-height) + 1px); + max-width: calc(var(--__line-height) + 1px); + min-height: calc(var(--__line-height) + 1px); + max-height: calc(var(--__line-height) + 1px); + line-height: var(--__line-height); text-align: center; border-radius: 0; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); - margin-right: 0.75em; + box-shadow: var(--shadow); + margin-right: var(--__horizontal-gap); &.menu-checkbox-checked::after { font-size: 1.25em; @@ -188,30 +137,5 @@ } } } - - .button-default.dropdown-item { - &, - i[class*="icon-"] { - color: $fallback--text; - color: var(--btnText, $fallback--text); - } - - &:active { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuPopoverText, $fallback--link); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - } - } } </style> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -87,7 +87,8 @@ const PostStatusForm = { 'fileLimit', 'submitOnEnter', 'emojiPickerPlacement', - 'optimisticPosting' + 'optimisticPosting', + 'profileMention' ], emits: [ 'posted', @@ -125,7 +126,7 @@ const PostStatusForm = { const { scopeCopy } = this.$store.getters.mergedConfig - if (this.replyTo) { + if (this.replyTo || this.profileMention) { const currentUser = this.$store.state.users.currentUser statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -161,7 +161,7 @@ v-model="newStatus.spoilerText" enable-emoji-picker :suggest="emojiSuggestor" - class="form-control" + class="input form-control" > <template #default="inputProps"> <input @@ -171,7 +171,7 @@ :disabled="posting && !optimisticPosting" v-bind="propsToNative(inputProps)" size="1" - class="form-post-subject" + class="input form-post-subject" > </template> </EmojiInput> @@ -180,7 +180,7 @@ v-model="newStatus.status" :suggest="emojiUserSuggestor" :placement="emojiPickerPlacement" - class="form-control main-input" + class="input form-control main-input" enable-emoji-picker hide-emoji-button :newline-on-ctrl-enter="submitOnEnter" @@ -198,7 +198,7 @@ rows="1" cols="1" :disabled="posting && !optimisticPosting" - class="form-post-body" + class="input form-post-body" :class="{ 'scrollable-form': !!maxHeight }" v-bind="propsToNative(inputProps)" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @@ -237,7 +237,7 @@ <Select id="post-content-type" v-model="newStatus.contentType" - class="form-control" + class="input form-control" :attrs="{ 'aria-label': $t('post_status.content_type_selection') }" > <option @@ -375,8 +375,6 @@ <script src="./post_status_form.js"></script> <style lang="scss"> -@import "../../variables"; - .post-status-form { position: relative; @@ -437,15 +435,12 @@ .preview-error { font-style: italic; - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } .preview-status { - border: 1px solid $fallback--border; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); padding: 0.5em; margin: 0; } @@ -456,8 +451,7 @@ .text-format { .only-format { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } } @@ -503,31 +497,6 @@ padding: 0 0.1em; display: flex; align-items: center; - - &.selected, - &:hover { - // needs to be specific to override icon default color - svg, - i, - label { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } - } - - &.disabled { - svg, - i { - cursor: not-allowed; - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - - &:hover { - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - } - } - } } .error { @@ -580,7 +549,7 @@ line-height: 1.85; } - .form-post-body { + .input.form-post-body { // TODO: make a resizable textarea component? box-sizing: content-box; // needed for easier computation of dynamic size overflow: hidden; @@ -591,6 +560,7 @@ height: calc(var(--post-line-height) * 1em); min-height: calc(var(--post-line-height) * 1em); resize: none; + background: transparent; &.scrollable-form { overflow-y: auto; @@ -609,8 +579,7 @@ margin: 0 0.5em; &.error { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); + color: var(--cRed); } } @@ -633,14 +602,10 @@ align-items: center; justify-content: center; opacity: 0.6; - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - border: 2px dashed $fallback--text; - border: 2px dashed var(--text, $fallback--text); + color: var(--text); + background-color: var(--bg); + border-radius: var(--roundness); + border: 2px dashed var(--text); } } </style> diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js @@ -63,6 +63,13 @@ const QuickFilterSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue @@ -16,39 +16,39 @@ > <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityAll" role="menuitemradio" @click="replyVisibilityAll = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityAll }" :aria-hidden="true" />{{ $t('settings.reply_visibility_all') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityFollowing" role="menuitemradio" @click="replyVisibilityFollowing = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }" :aria-hidden="true" />{{ $t('settings.reply_visibility_following_short') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilitySelf" role="menuitemradio" @click="replyVisibilitySelf = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilitySelf }" :aria-hidden="true" />{{ $t('settings.reply_visibility_self_short') }} @@ -60,43 +60,55 @@ /> </div> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="muteBotStatuses" @click="muteBotStatuses = !muteBotStatuses" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': muteBotStatuses }" :aria-hidden="true" />{{ $t('settings.mute_bot_posts') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" + role="menuitemcheckbox" + :aria-checked="muteSensitiveStatuses" + @click="muteSensitiveStatuses = !muteSensitiveStatuses" + > + <span + class="input menu-checkbox" + :class="{ 'menu-checkbox-checked': muteSensitiveStatuses }" + :aria-hidden="true" + />{{ $t('settings.mute_sensitive_posts') }} + </button> + <button + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMedia" @click="hideMedia = !hideMedia" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMedia }" :aria-hidden="true" />{{ $t('settings.hide_media_previews') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMutedPosts" @click="hideMutedPosts = !hideMutedPosts" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMutedPosts }" :aria-hidden="true" />{{ $t('settings.hide_all_muted_posts') }} </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click="openTab('filtering')" > diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js @@ -52,7 +52,6 @@ const QuickViewSettings = { get () { return this.mergedConfig.mentionLinkShowAvatar }, set () { const value = !this.showUserAvatars - console.log(value) this.$store.dispatch('setOption', { name: 'mentionLinkShowAvatar', value }) } }, @@ -62,6 +61,13 @@ const QuickViewSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue @@ -12,13 +12,13 @@ > <div role="group"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="conversationDisplay === 'tree'" role="menuitemradio" @click="conversationDisplay = 'tree'" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :aria-hidden="true" :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }" /><FAIcon @@ -27,13 +27,13 @@ /> {{ $t('settings.conversation_display_tree_quick') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="conversationDisplay === 'linear'" role="menuitemradio" @click="conversationDisplay = 'linear'" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }" :aria-hidden="true" /><FAIcon @@ -47,45 +47,45 @@ class="dropdown-divider" /> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="showUserAvatars" @click="showUserAvatars = !showUserAvatars" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': showUserAvatars }" :aria-hidden="true" />{{ $t('settings.mention_link_show_avatar_quick') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="autoUpdate" @click="autoUpdate = !autoUpdate" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': autoUpdate }" :aria-hidden="true" />{{ $t('settings.auto_update') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="collapseWithSubjects" @click="collapseWithSubjects = !collapseWithSubjects" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': collapseWithSubjects }" :aria-hidden="true" />{{ $t('settings.collapse_subject') }} </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click="openTab('general')" > diff --git a/src/components/quotes_timeline/quotes_timeline.js b/src/components/quotes_timeline/quotes_timeline.js @@ -0,0 +1,26 @@ +import Timeline from '../timeline/timeline.vue' + +const QuotesTimeline = { + created () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + }, + components: { + Timeline + }, + computed: { + statusId () { return this.$route.params.id }, + timeline () { return this.$store.state.statuses.timelines.quotes } + }, + watch: { + statusId () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + } + }, + unmounted () { + this.$store.dispatch('stopFetchingTimeline', 'quotes') + } +} + +export default QuotesTimeline diff --git a/src/components/quotes_timeline/quotes_timeline.vue b/src/components/quotes_timeline/quotes_timeline.vue @@ -0,0 +1,10 @@ +<template> + <Timeline + :title="$t('nav.quotes')" + :timeline="timeline" + :timeline-name="'quotes'" + :status-id="statusId" + /> +</template> + +<script src='./quotes_timeline.js'></script> diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue @@ -14,7 +14,7 @@ v-if="typeof fallback !== 'undefined'" :id="name + '-o'" :aria-labelledby="name + '-label'" - class="opt visible-for-screenreader-only" + class="input -checkbox opt visible-for-screenreader-only" type="checkbox" :checked="present" @change="$emit('update:modelValue', !present ? fallback : undefined)" @@ -27,7 +27,7 @@ /> <input :id="name" - class="input-number" + class="input input-number" type="range" :value="modelValue || fallback" :disabled="!present || disabled" @@ -38,7 +38,7 @@ > <input :id="name + '-numeric'" - class="input-number" + class="input input-number" type="number" :aria-labelledby="name + '-label'" :value="modelValue || fallback" diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue @@ -41,7 +41,6 @@ <script src="./react_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ReactButton { @@ -58,7 +57,7 @@ height: 1px; width: 100%; margin: 0.5em; - background-color: var(--border, $fallback--border); + background-color: var(--border); } .reaction-picker { @@ -99,11 +98,6 @@ padding: 10px; margin: -10px; - &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); - } - @include unfocused-style { .focus-marker { visibility: hidden; diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js @@ -83,6 +83,8 @@ const registration = { signedIn: (state) => !!state.users.currentUser, isPending: (state) => state.users.signUpPending, serverValidationErrors: (state) => state.users.signUpErrors, + signUpNotice: (state) => state.users.signUpNotice, + hasSignUpNotice: (state) => !!state.users.signUpNotice.message, termsOfService: (state) => state.instance.tos, accountActivationRequired: (state) => state.instance.accountActivationRequired, accountApprovalRequired: (state) => state.instance.accountApprovalRequired, @@ -107,8 +109,12 @@ const registration = { if (!this.v$.$invalid) { try { - await this.signUp(this.user) - this.$router.push({ name: 'friends' }) + const status = await this.signUp(this.user) + if (status === 'ok') { + this.$router.push({ name: 'friends' }) + } + // If status is not 'ok' (i.e. it needs further actions to be done + // before you can login), display sign up notice, do not switch anywhere } catch (error) { console.warn('Registration failed: ', error) this.setCaptcha() diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue @@ -3,7 +3,10 @@ <div class="panel-heading"> {{ $t('registration.registration') }} </div> - <div class="panel-body"> + <div + v-if="!hasSignUpNotice" + class="panel-body" + > <form class="registration-form" @submit.prevent="submit(user)" @@ -22,7 +25,7 @@ id="sign-up-username" v-model.trim="v$.user.username.$model" :disabled="isPending" - class="form-control" + class="input form-control" :aria-required="true" :placeholder="$t('registration.username_placeholder')" > @@ -50,7 +53,7 @@ id="sign-up-fullname" v-model.trim="v$.user.fullname.$model" :disabled="isPending" - class="form-control" + class="input form-control" :aria-required="true" :placeholder="$t('registration.fullname_placeholder')" > @@ -78,7 +81,7 @@ id="email" v-model="v$.user.email.$model" :disabled="isPending" - class="form-control" + class="input form-control" type="email" :aria-required="accountActivationRequired" > @@ -103,7 +106,7 @@ id="bio" v-model="user.bio" :disabled="isPending" - class="form-control" + class="input form-control" :placeholder="bioPlaceholder" /> </div> @@ -120,7 +123,7 @@ id="sign-up-password" v-model="user.password" :disabled="isPending" - class="form-control" + class="input form-control" type="password" :aria-required="true" > @@ -148,7 +151,7 @@ id="sign-up-password-confirmation" v-model="user.confirm" :disabled="isPending" - class="form-control" + class="input form-control" type="password" :aria-required="true" > @@ -181,7 +184,7 @@ id="sign-up-birthday" v-model="user.birthday" :disabled="isPending" - class="form-control" + class="input form-control" type="date" :max="birthdayRequired ? birthdayMinAttr : undefined" :aria-required="birthdayRequired" @@ -226,7 +229,7 @@ id="reason" v-model="user.reason" :disabled="isPending" - class="form-control" + class="input form-control" :placeholder="reasonPlaceholder" /> </div> @@ -253,7 +256,7 @@ id="captcha-answer" v-model="captcha.solution" :disabled="isPending" - class="form-control" + class="input form-control" type="text" autocomplete="off" autocorrect="off" @@ -272,7 +275,7 @@ id="token" v-model="token" disabled="true" - class="form-control" + class="input form-control" type="text" > </div> @@ -307,14 +310,16 @@ </div> </form> </div> + <div v-else> + <p class="registration-notice"> + {{ signUpNotice.message }} + </p> + </div> </div> </template> <script src="./registration.js"></script> <style lang="scss"> -@import "../../variables"; -$validations-cRed: #f04124; - .registration-form { display: flex; flex-direction: column; @@ -361,8 +366,7 @@ $validations-cRed: #f04124; } .form-group--error .form--label { - color: $validations-cRed; - color: var(--cRed, $validations-cRed); + color: var(--cRed); } .form-error { @@ -404,6 +408,10 @@ $validations-cRed: #f04124; } } +.registration-notice { + margin: 0.6em; +} + @media all and (max-width: 800px) { .registration-form .container { flex-direction: column-reverse; diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue @@ -59,7 +59,6 @@ <script src="./reply_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ReplyButton { @@ -78,8 +77,7 @@ .interactive { &:hover .svg-inline--fa, &.-active .svg-inline--fa { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } @include unfocused-style { diff --git a/src/components/report/report.js b/src/components/report/report.js @@ -16,7 +16,6 @@ const Report = { }, computed: { report () { - console.log(this.$store.state.reports.reports[this.reportId] || {}) return this.$store.state.reports.reports[this.reportId] || {} }, state: { diff --git a/src/components/report/report.scss b/src/components/report/report.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Report { .report-content { margin: 0.5em 0 1em; @@ -10,12 +8,8 @@ } .reported-status { - border: 1px solid $fallback--faint; - border-color: var(--faint, $fallback--faint); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); - color: $fallback--text; - color: var(--text, $fallback--text); + border: 1px solid var(--border); + border-radius: var(--roundness); display: block; padding: 0.5em; margin: 0.5em 0; diff --git a/src/components/report/report.vue b/src/components/report/report.vue @@ -17,7 +17,7 @@ <Select :id="report-state" v-model="state" - class="form-control" + class="input form-control" > <option v-for="state in ['open', 'closed', 'resolved']" diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue @@ -84,7 +84,6 @@ <script src="./retweet_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .RetweetButton { @@ -107,8 +106,7 @@ &:hover .svg-inline--fa, &.-repeated .svg-inline--fa { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } @include unfocused-style { diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx @@ -79,6 +79,12 @@ export default { required: false, type: Boolean, default: false + }, + // Faint style (for notifs) + faint: { + required: false, + type: Boolean, + default: false } }, // NEVER EVER TOUCH DATA INSIDE RENDER @@ -277,7 +283,7 @@ export default { // DO NOT USE SLOTS they cause a re-render feedback loop here. // slots updated -> rerender -> emit -> update up the tree -> rerender -> ... // at least until vue3? - const result = <span class="RichContent"> + const result = <span class={['RichContent', this.faint ? '-faint' : '']}> { pass2 } </span> diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss @@ -1,10 +1,19 @@ -@import "../../variables"; - .RichContent { + font-family: var(--font); + + &.-faint { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ + } + blockquote { margin: 0.2em 0 0.2em 0.2em; font-style: italic; - border-left: 0.2em solid var(--faint, $fallback--faint); + border-left: 0.2em solid var(--textFaint); padding-left: 1em; } @@ -17,7 +26,7 @@ kbd, var, pre { - font-family: var(--postCodeFont, monospace); + font-family: var(--monoFont); } p { @@ -65,4 +74,17 @@ vertical-align: middle; object-fit: contain; } + + .greentext { + color: var(--funtextGreentext); + } + + .cyantext { + color: var(--funtextCyantext); + } +} + +a .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + color: var(--link) !important; } diff --git a/src/components/rich_content/rich_content.style.js b/src/components/rich_content/rich_content.style.js @@ -0,0 +1,18 @@ +export default { + name: 'RichContent', + selector: '.RichContent', + validInnerComponents: [ + 'Text', + 'FunText', + 'Link' + ], + defaultRules: [ + { + directives: { + '--font': 'generic | inherit', + '--monoFont': 'generic | monospace', + textNoCssColor: 'yes' + } + } + ] +} diff --git a/src/components/root.style.js b/src/components/root.style.js @@ -0,0 +1,49 @@ +export default { + name: 'Root', + selector: ':root', + validInnerComponents: [ + 'Underlay', + 'Modals', + 'Popover', + 'TopBar', + 'Scrollbar', + 'ScrollbarElement', + 'MobileDrawer', + 'Alert', + 'Button' // mobile post button + ], + validInnerComponentsLite: [ + 'Underlay', + 'Scrollbar', + 'ScrollbarElement' + ], + defaultRules: [ + { + directives: { + // These are here just to establish order, + // themes should override those + '--bg': 'color | #121a24', + '--fg': 'color | #182230', + '--text': 'color | #b9b9ba', + '--link': 'color | #d8a070', + '--accent': 'color | #d8a070', + '--cRed': 'color | #FF0000', + '--cBlue': 'color | #0095ff', + '--cGreen': 'color | #0fa00f', + '--cOrange': 'color | #ffa500', + + // Fonts + '--font': 'generic | sans-serif', + '--monoFont': 'generic | monospace', + + // Fallback no-background-image color + // (also useful in some other places like scrollbars) + '--wallpaper': 'color | --bg, -2', + + // Selection colors + '--selectionBackground': 'color | --accent', + '--selectionText': 'color | $textColor(--accent, --text, no-preserve)' + } + } + ] +} diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js @@ -44,10 +44,10 @@ const ScopeSelector = { }, css () { return { - public: { selected: this.currentScope === 'public' }, - unlisted: { selected: this.currentScope === 'unlisted' }, - private: { selected: this.currentScope === 'private' }, - direct: { selected: this.currentScope === 'direct' } + public: { toggled: this.currentScope === 'public' }, + unlisted: { toggled: this.currentScope === 'unlisted' }, + private: { toggled: this.currentScope === 'private' }, + direct: { toggled: this.currentScope === 'direct' } } } }, diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue @@ -64,8 +64,6 @@ <script src="./scope_selector.js"></script> <style lang="scss"> -@import "../../variables"; - .ScopeSelector { .scope { display: inline-block; @@ -73,11 +71,6 @@ min-width: 1.3em; min-height: 1.3em; text-align: center; - - &.selected svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } } </style> diff --git a/src/components/screen_reader_notice/screen_reader_notice.js b/src/components/screen_reader_notice/screen_reader_notice.js @@ -2,7 +2,7 @@ const ScreenReaderNotice = { props: { ariaLive: { type: String, - defualt: 'assertive' + default: 'assertive' } }, data () { diff --git a/src/components/scrollbar.style.js b/src/components/scrollbar.style.js @@ -0,0 +1,11 @@ +export default { + name: 'Scrollbar', + selector: '::-webkit-scrollbar', + defaultRules: [ + { + directives: { + background: '--wallpaper' + } + } + ] +} diff --git a/src/components/scrollbar_element.style.js b/src/components/scrollbar_element.style.js @@ -0,0 +1,101 @@ +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) + +const buttonInsetFakeBorders = [border(true, false), border(false, true)] +const inputInsetFakeBorders = [border(true, true), border(false, false)] +const buttonOuterShadow = { + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 +} + +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + +export default { + name: 'ScrollbarElement', + selector: '::-webkit-scrollbar-button', + states: { + pressed: ':active', + hover: ':hover:not(:disabled)', + disabled: ':disabled' + }, + validInnerComponents: [ + 'Text' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: [buttonOuterShadow, ...buttonInsetFakeBorders], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: [hoverGlow, ...buttonInsetFakeBorders] + } + }, + { + state: ['pressed'], + directives: { + shadow: [buttonOuterShadow, ...inputInsetFakeBorders] + } + }, + { + state: ['hover', 'pressed'], + directives: { + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + }, + { + state: ['toggled'], + directives: { + background: '--accent,-24.2', + shadow: [buttonOuterShadow, ...inputInsetFakeBorders] + } + }, + { + state: ['toggled', 'hover'], + directives: { + background: '--accent,-24.2', + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: [...buttonInsetFakeBorders] + } + }, + { + component: 'Text', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/search/search.vue b/src/components/search/search.vue @@ -1,15 +1,15 @@ <template> - <div class="panel panel-default"> + <div class="Search panel panel-default"> <div class="panel-heading"> <div class="title"> {{ $t('nav.search') }} </div> </div> - <div class="search-input-container"> + <div class="panel-body search-input-container"> <input ref="searchInput" v-model="searchTerm" - class="search-input" + class="input search-input" :placeholder="$t('nav.search')" @keyup.enter="newQuery(searchTerm)" > @@ -23,7 +23,7 @@ </div> <div v-if="loading && statusesOffset == 0" - class="text-center loading-icon" + class="panel-body text-center loading-icon" > <FAIcon icon="circle-notch" @@ -67,7 +67,7 @@ /> <button v-if="!loading && loaded && lastStatusFetchCount > 0" - class="more-statuses-button button-unstyled -link -fullwidth" + class="more-statuses-button button-unstyled -link" @click.prevent="search(searchTerm, 'statuses')" > <div class="new-status-notification text-center"> @@ -148,11 +148,8 @@ <script src="./search.js"></script> <style lang="scss"> -@import "../../variables"; - .search-result-heading { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--faint); padding: 0.75rem; text-align: center; } @@ -171,17 +168,7 @@ .search-result { box-sizing: border-box; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); -} - -.search-result-footer { - border-width: 1px 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - padding: 10px; - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + border-color: var(--border); } .search-input-container { @@ -212,8 +199,7 @@ .hashtag { flex: 1 1 auto; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -226,14 +212,14 @@ line-height: 2.25rem; font-weight: 500; text-align: center; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } .more-statuses-button { height: 3.5em; line-height: 3.5em; + width: 100%; } </style> diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue @@ -22,7 +22,7 @@ id="search-bar-input" ref="searchInput" v-model="searchTerm" - class="search-bar-input" + class="input search-bar-input" :placeholder="$t('nav.search')" type="text" @keyup.enter="find(searchTerm)" @@ -60,8 +60,6 @@ <script src="./search_bar.js"></script> <style lang="scss"> -@import "../../variables"; - .SearchBar { display: inline-flex; align-items: baseline; @@ -86,8 +84,7 @@ } .cancel-icon { - color: $fallback--text; - color: var(--btnTopBarText, $fallback--text); + color: var(--text); } } diff --git a/src/components/select/select.vue b/src/components/select/select.vue @@ -22,8 +22,6 @@ <script src="./select.js"> </script> <style lang="scss"> -@import "../../variables"; - /* TODO fix order of styles */ label.Select { padding: 0; @@ -32,12 +30,10 @@ label.Select { appearance: none; background: transparent; border: none; - color: $fallback--text; - color: var(--inputText, --text, $fallback--text); + color: var(--text); margin: 0; padding: 0 2em 0 0.2em; - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); + font-family: var(--font); font-size: 1em; width: 100%; z-index: 1; @@ -52,8 +48,7 @@ label.Select { right: 5px; height: 100%; width: 0.875em; - color: $fallback--text; - color: var(--inputText, $fallback--text); + font-family: var(--font); line-height: 2; z-index: 0; pointer-events: none; diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue @@ -23,16 +23,19 @@ <List :items="items" :get-key="getKey" + :get-class="item => isSelected(item) ? '-active' : ''" > <template #item="{item}"> <div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }" + @click.stop="toggle(!isSelected(item), item)" > <div class="selectable-list-checkbox-wrapper"> <Checkbox :model-value="isSelected(item)" @update:model-value="checked => toggle(checked, item)" + @click.stop /> </div> <slot @@ -51,9 +54,11 @@ <script src="./selectable_list.js"></script> <style lang="scss"> -@import "../../variables"; - .selectable-list { + --__line-height: 1.5em; + --__horizontal-gap: 0.75em; + --__vertical-gap: 0.5em; + &-item-inner { display: flex; align-items: center; @@ -63,24 +68,12 @@ } } - &-item-selected-inner { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - &-header { display: flex; align-items: center; - padding: 0.6em 0; - border-bottom: 2px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + padding: var(--__vertical-gap) var(--__horizontal-gap); + border-bottom: 1px solid; + border-bottom-color: var(--border); &-actions { flex: 1; @@ -88,7 +81,7 @@ } &-checkbox-wrapper { - padding: 0 10px; + padding-right: var(--__horizontal-gap); flex: none; } } diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js @@ -0,0 +1,257 @@ +import { clone, assign } from 'lodash' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import StringSetting from '../helpers/string_setting.vue' +import Checkbox from 'components/checkbox/checkbox.vue' +import StillImage from 'components/still-image/still-image.vue' +import Select from 'components/select/select.vue' +import Popover from 'components/popover/popover.vue' +import ConfirmModal from 'components/confirm_modal/confirm_modal.vue' +import ModifiedIndicator from '../helpers/modified_indicator.vue' +import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue' + +const EmojiTab = { + components: { + TabSwitcher, + StringSetting, + Checkbox, + StillImage, + Select, + Popover, + ConfirmModal, + ModifiedIndicator, + EmojiEditingPopover + }, + + data () { + return { + knownLocalPacks: { }, + knownRemotePacks: { }, + editedMetadata: { }, + packName: '', + newPackName: '', + deleteModalVisible: false, + remotePackInstance: '', + remotePackDownloadAs: '' + } + }, + + provide () { + return { emojiAddr: this.emojiAddr } + }, + + computed: { + pack () { + return this.packName !== '' ? this.knownPacks[this.packName] : undefined + }, + packMeta () { + if (this.editedMetadata[this.packName] === undefined) { + this.editedMetadata[this.packName] = clone(this.pack.pack) + } + + return this.editedMetadata[this.packName] + }, + knownPacks () { + // Copy the object itself but not the children, so they are still passed by reference and modified + const result = clone(this.knownLocalPacks) + for (const instName in this.knownRemotePacks) { + for (const instPack in this.knownRemotePacks[instName]) { + result[`${instPack}@${instName}`] = this.knownRemotePacks[instName][instPack] + } + } + + return result + }, + downloadWillReplaceLocal () { + return (this.remotePackDownloadAs.trim() === '' && this.pack.remote && this.pack.remote.baseName in this.knownLocalPacks) || + (this.remotePackDownloadAs in this.knownLocalPacks) + } + }, + + methods: { + reloadEmoji () { + this.$store.state.api.backendInteractor.reloadEmoji() + }, + importFromFS () { + this.$store.state.api.backendInteractor.importEmojiFromFS() + }, + emojiAddr (name) { + if (this.pack.remote !== undefined) { + // Remote pack + return `${this.pack.remote.instance}/emoji/${encodeURIComponent(this.pack.remote.baseName)}/${name}` + } else { + return `${this.$store.state.instance.server}/emoji/${encodeURIComponent(this.packName)}/${name}` + } + }, + + createEmojiPack () { + this.$store.state.api.backendInteractor.createEmojiPack( + { name: this.newPackName } + ).then(resp => resp.json()).then(resp => { + if (resp === 'ok') { + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + this.$refs.createPackPopover.hidePopover() + + this.packName = this.newPackName + this.newPackName = '' + }) + }, + deleteEmojiPack () { + this.$store.state.api.backendInteractor.deleteEmojiPack( + { name: this.packName } + ).then(resp => resp.json()).then(resp => { + if (resp === 'ok') { + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + delete this.editedMetadata[this.packName] + + this.deleteModalVisible = false + this.packName = '' + }) + }, + + metaEdited (prop) { + if (!this.pack) return + + const def = this.pack.pack[prop] || '' + const edited = this.packMeta[prop] || '' + return edited !== def + }, + savePackMetadata () { + this.$store.state.api.backendInteractor.saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }).then( + resp => resp.json() + ).then(resp => { + if (resp.error !== undefined) { + this.displayError(resp.error) + return + } + + // Update actual pack data + this.pack.pack = resp + // Delete edited pack data, should auto-update itself + delete this.editedMetadata[this.packName] + }) + }, + + updatePackFiles (newFiles) { + this.pack.files = newFiles + this.sortPackFiles(this.packName) + }, + + loadPacksPaginated (listFunction) { + const pageSize = 25 + const allPacks = {} + + return listFunction({ instance: this.remotePackInstance, page: 1, pageSize: 0 }) + .then(data => data.json()) + .then(data => { + if (data.error !== undefined) { return Promise.reject(data.error) } + + let resultingPromise = Promise.resolve({}) + for (let i = 0; i < Math.ceil(data.count / pageSize); i++) { + resultingPromise = resultingPromise.then(() => listFunction({ instance: this.remotePackInstance, page: i, pageSize }) + ).then(data => data.json()).then(pageData => { + if (pageData.error !== undefined) { return Promise.reject(pageData.error) } + + assign(allPacks, pageData.packs) + }) + } + + return resultingPromise + }) + .then(finished => allPacks) + .catch(data => { + this.displayError(data) + }) + }, + + refreshPackList () { + this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks) + .then(allPacks => { + this.knownLocalPacks = allPacks + for (const name of Object.keys(this.knownLocalPacks)) { + this.sortPackFiles(name) + } + }) + }, + listRemotePacks () { + this.loadPacksPaginated(this.$store.state.api.backendInteractor.listRemoteEmojiPacks) + .then(allPacks => { + let inst = this.remotePackInstance + if (!inst.startsWith('http')) { inst = 'https://' + inst } + const instUrl = new URL(inst) + inst = instUrl.host + + for (const packName in allPacks) { + allPacks[packName].remote = { + baseName: packName, + instance: instUrl.origin + } + } + + this.knownRemotePacks[inst] = allPacks + for (const pack in this.knownRemotePacks[inst]) { + this.sortPackFiles(`${pack}@${inst}`) + } + + this.$refs.remotePackPopover.hidePopover() + }) + .catch(data => { + this.displayError(data) + }) + }, + downloadRemotePack () { + if (this.remotePackDownloadAs.trim() === '') { + this.remotePackDownloadAs = this.pack.remote.baseName + } + + this.$store.state.api.backendInteractor.downloadRemoteEmojiPack({ + instance: this.pack.remote.instance, packName: this.pack.remote.baseName, as: this.remotePackDownloadAs + }) + .then(data => data.json()) + .then(resp => { + if (resp === 'ok') { + this.$refs.dlPackPopover.hidePopover() + + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + this.packName = this.remotePackDownloadAs + this.remotePackDownloadAs = '' + }) + }, + displayError (msg) { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'admin_dash.emoji.error', + messageArgs: [msg], + level: 'error' + }) + }, + sortPackFiles (nameOfPack) { + // Sort by key + const sorted = Object.keys(this.knownPacks[nameOfPack].files).sort().reduce((acc, key) => { + if (key.length === 0) return acc + acc[key] = this.knownPacks[nameOfPack].files[key] + return acc + }, {}) + this.knownPacks[nameOfPack].files = sorted + } + }, + + mounted () { + this.refreshPackList() + } +} + +export default EmojiTab diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss @@ -0,0 +1,59 @@ +.emoji-tab { + .btn-group .btn:not(:first-child) { + margin-left: 0.5em; + } + + .pack-info-wrapper { + margin-top: 1em; + } + + .emoji-info-input { + width: 100%; + } + + .emoji-data-input { + width: 40%; + margin-left: 0.5em; + margin-right: 0.5em; + } + + .emoji { + width: 32px; + height: 32px; + } + + .emoji-unsaved { + box-shadow: 0 3px 5px var(--cBlue); + } + + .emoji-list { + display: flex; + flex-wrap: wrap; + gap: 1em 1em; + } +} + +.emoji-tab-popover-button:not(:first-child) { + margin-left: 0.5em; +} + +.emoji-tab-popover-input { + margin-bottom: 0.5em; + + label { + display: block; + margin-bottom: 0.5em; + } + + input { + width: 20em; + } + + .emoji-tab-popover-file { + padding-top: 3px; + } + + .warning { + color: var(--cOrange); + } +} diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue @@ -0,0 +1,358 @@ +<template> + <div + class="emoji-tab" + :label="$t('admin_dash.tabs.emoji')" + > + <div class="setting-item"> + <h2>{{ $t('admin_dash.tabs.emoji') }}</h2> + + <ul class="setting-list"> + <h3>{{ $t('admin_dash.emoji.global_actions') }}</h3> + + <li class="btn-group setting-item"> + <button + class="button button-default btn" + type="button" + @click="reloadEmoji" + > + {{ $t('admin_dash.emoji.reload') }} + </button> + <button + class="button button-default btn" + type="button" + @click="importFromFS" + > + {{ $t('admin_dash.emoji.importFS') }} + </button> + </li> + + <li class="btn-group setting-item"> + <button + class="button button-default btn" + type="button" + @click="$refs.remotePackPopover.showPopover" + > + {{ $t('admin_dash.emoji.remote_packs') }} + + <Popover + ref="remotePackPopover" + popover-class="emoji-tab-edit-popover popover-default" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <div class="emoji-tab-popover-input"> + <h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3> + <input + v-model="remotePackInstance" + class="input" + :placeholder="$t('admin_dash.emoji.remote_pack_instance')" + > + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="listRemotePacks" + > + {{ $t('admin_dash.emoji.do_list') }} + </button> + </div> + </template> + </Popover> + </button> + </li> + + <h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3> + + <li> + <h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4> + + <Select + v-model="packName" + class="form-control" + > + <option + value="" + disabled + hidden + > + {{ $t('admin_dash.emoji.emoji_pack') }} + </option> + <option + v-for="(pack, listPackName) in knownPacks" + :key="listPackName" + :label="listPackName" + > + {{ listPackName }} + </option> + </Select> + + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="$refs.createPackPopover.showPopover" + > + {{ $t('admin_dash.emoji.create_pack') }} + </button> + <Popover + ref="createPackPopover" + popover-class="emoji-tab-edit-popover popover-default" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <div class="emoji-tab-popover-input"> + <h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3> + <input + v-model="newPackName" + :placeholder="$t('admin_dash.emoji.new_pack_name')" + class="input" + > + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="createEmojiPack" + > + {{ $t('admin_dash.emoji.create') }} + </button> + </div> + </template> + </Popover> + </li> + </ul> + + <div v-if="pack"> + <div class="pack-info-wrapper"> + <ul class="setting-list"> + <li> + <label> + {{ $t('admin_dash.emoji.description') }} + <ModifiedIndicator + :changed="metaEdited('description')" + message-key="admin_dash.emoji.metadata_changed" + /> + + <textarea + v-model="packMeta.description" + :disabled="pack.remote !== undefined" + class="bio resize-height input" + /> + </label> + </li> + <li> + <label> + {{ $t('admin_dash.emoji.homepage') }} + <ModifiedIndicator + :changed="metaEdited('homepage')" + message-key="admin_dash.emoji.metadata_changed" + /> + + <input + v-model="packMeta.homepage" + class="emoji-info-input input" + :disabled="pack.remote !== undefined" + > + </label> + </li> + <li> + <label> + {{ $t('admin_dash.emoji.fallback_src') }} + <ModifiedIndicator + :changed="metaEdited('fallback-src')" + message-key="admin_dash.emoji.metadata_changed" + /> + + <input + v-model="packMeta['fallback-src']" + class="emoji-info-input input" + :disabled="pack.remote !== undefined" + > + </label> + </li> + <li> + <label> + {{ $t('admin_dash.emoji.fallback_sha256') }} + + <input + v-model="packMeta['fallback-src-sha256']" + :disabled="true" + class="emoji-info-input input" + > + </label> + </li> + <li> + <Checkbox + v-model="packMeta['share-files']" + :disabled="pack.remote !== undefined" + > + {{ $t('admin_dash.emoji.share') }} + </Checkbox> + + <ModifiedIndicator + :changed="metaEdited('share-files')" + message-key="admin_dash.emoji.metadata_changed" + /> + </li> + <li class="btn-group"> + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="savePackMetadata" + > + {{ $t('admin_dash.emoji.save_meta') }} + </button> + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="savePackMetadata" + > + {{ $t('admin_dash.emoji.revert_meta') }} + </button> + + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="deleteModalVisible = true" + > + {{ $t('admin_dash.emoji.delete_pack') }} + + <ConfirmModal + v-if="deleteModalVisible" + :title="$t('admin_dash.emoji.delete_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="deleteModalVisible = false" + @accepted="deleteEmojiPack" + > + {{ $t('admin_dash.emoji.delete_confirm', [packName]) }} + </ConfirmModal> + </button> + + <button + v-if="pack.remote !== undefined" + class="button button-default btn" + type="button" + @click="$refs.dlPackPopover.showPopover" + > + {{ $t('admin_dash.emoji.download_pack') }} + + <Popover + ref="dlPackPopover" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + popover-class="emoji-tab-edit-popover popover-default" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3> + <div> + <div> + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.download_as_name') }} + <input + v-model="remotePackDownloadAs" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.download_as_name_full')" + > + </label> + + <div + v-if="downloadWillReplaceLocal" + class="warning" + > + <em>{{ $t('admin_dash.emoji.replace_warning') }}</em> + </div> + </div> + + <button + class="button button-default btn" + type="button" + @click="downloadRemotePack" + > + {{ $t('admin_dash.emoji.download') }} + </button> + </div> + </div> + </template> + </Popover> + </button> + </li> + </ul> + </div> + + <ul class="setting-list"> + <h4> + {{ $t('admin_dash.emoji.files') }} + + <ModifiedIndicator + v-if="pack" + :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)" + message-key="admin_dash.emoji.emoji_changed" + /> + </h4> + + <div + v-if="pack" + class="emoji-list" + > + <EmojiEditingPopover + v-if="pack.remote === undefined" + placement="bottom" + new-upload + :title="$t('admin_dash.emoji.adding_new')" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" + > + <template #trigger> + <FAIcon + icon="plus" + size="2x" + :title="$t('admin_dash.emoji.add_file')" + /> + </template> + </EmojiEditingPopover> + + <EmojiEditingPopover + v-for="(file, shortcode) in pack.files" + ref="emojiPopovers" + :key="shortcode" + placement="top" + :title="$t('admin_dash.emoji.editing', [shortcode])" + :disabled="pack.remote !== undefined" + :shortcode="shortcode" + :file="file" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" + > + <template #trigger> + <StillImage + class="emoji" + :src="emojiAddr(file)" + :title="`:${shortcode}:`" + :alt="`:${shortcode}:`" + /> + </template> + </EmojiEditingPopover> + </div> + </ul> + </div> + </div> + </div> +</template> + +<script src="./emoji_tab.js"></script> + +<style lang="scss" src="./emoji_tab.scss"></style> diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js @@ -55,9 +55,13 @@ const FrontendsTab = { return fe.refs.includes(frontend.ref) }, getSuggestedRef (frontend) { - const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary'] - if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) { - return defaultFe.ref + if (this.adminDraft) { + const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary'] + if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) { + return defaultFe.ref + } else { + return frontend.refs[0] + } } else { return frontend.refs[0] } diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue @@ -6,7 +6,10 @@ <div class="setting-item"> <h2>{{ $t('admin_dash.tabs.frontends') }}</h2> <p>{{ $t('admin_dash.frontend.wip_notice') }}</p> - <ul class="setting-list"> + <ul + v-if="adminDraft" + class="setting-list" + > <li> <h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3> <p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p> @@ -23,8 +26,18 @@ </ul> </li> </ul> + <div + v-else + class="setting-list" + > + {{ $t('admin_dash.frontend.default_frontend_unavail') }} + </div> + <div class="setting-list relative"> - <PanelLoading class="overlay" v-if="working"/> + <PanelLoading + v-if="working" + class="overlay" + /> <h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3> <ul class="cards-list"> <li @@ -33,9 +46,9 @@ > <strong>{{ frontend.name }}</strong> {{ ' ' }} - <span v-if="adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name"> + <span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name"> <i18n-t - v-if="adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]" + v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]" keypath="admin_dash.frontend.is_default" /> <i18n-t @@ -43,7 +56,7 @@ keypath="admin_dash.frontend.is_default_custom" > <template #version> - <code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> + <code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> </template> </i18n-t> </span> @@ -103,7 +116,7 @@ <button v-for="ref in frontend.refs" :key="ref" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click.prevent="update(frontend, ref)" @click="close" > @@ -134,7 +147,7 @@ class="button button-default btn" type="button" :disabled=" - adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name && + !adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0] " @click="setDefault(frontend)" @@ -160,7 +173,7 @@ <button v-for="ref in frontend.installedRefs || frontend.refs" :key="ref" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click.prevent="setDefault(frontend, ref)" @click="close" > diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue @@ -8,7 +8,10 @@ </li> <!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 --> <li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined"> - <AttachmentSetting compact path=":pleroma.:instance.:favicon" /> + <AttachmentSetting + compact + path=":pleroma.:instance.:favicon" + /> </li> <li> <StringSetting path=":pleroma.:instance.:email" /> @@ -20,7 +23,10 @@ <StringSetting path=":pleroma.:instance.:short_description" /> </li> <li> - <AttachmentSetting compact path=":pleroma.:instance.:instance_thumbnail" /> + <AttachmentSetting + compact + path=":pleroma.:instance.:instance_thumbnail" + /> </li> <li> <AttachmentSetting path=":pleroma.:instance.:background_image" /> diff --git a/src/components/settings_modal/helpers/attachment_setting.vue b/src/components/settings_modal/helpers/attachment_setting.vue @@ -29,7 +29,7 @@ <label for="path">{{ $t('settings.url') }}</label> <input :id="path" - class="string-input" + class="input string-input" :disabled="shouldBeDisabled" :value="realDraftMode ? draft : state" @change="update" diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue @@ -0,0 +1,227 @@ +<template> + <Popover + ref="emojiPopover" + trigger="click" + :placement="placement" + bound-to-selector=".emoji-list" + popover-class="emoji-tab-edit-popover popover-default" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + :disabled="disabled" + :class="{'emoji-unsaved': isEdited}" + > + <template #trigger> + <slot name="trigger" /> + </template> + <template #content> + <h3> + {{ title }} + </h3> + + <StillImage + v-if="emojiPreview" + class="emoji" + :src="emojiPreview" + /> + <div + v-else + class="emoji" + /> + + <div + v-if="newUpload" + class="emoji-tab-popover-input" + > + <input + type="file" + accept="image/*" + class="emoji-tab-popover-file input" + @change="uploadFile = $event.target.files" + > + </div> + <div> + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.shortcode') }} + <input + v-model="editedShortcode" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_shortcode')" + > + </label> + </div> + + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.filename') }} + + <input + v-model="editedFile" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_filename')" + > + </label> + </div> + + <button + class="button button-default btn" + type="button" + :disabled="newUpload ? uploadFile.length == 0 : !isEdited" + @click="newUpload ? uploadEmoji() : saveEditedEmoji()" + > + {{ $t('admin_dash.emoji.save') }} + </button> + + <template v-if="!newUpload"> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="deleteModalVisible = true" + > + {{ $t('admin_dash.emoji.delete') }} + </button> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="revertEmoji" + > + {{ $t('admin_dash.emoji.revert') }} + </button> + <ConfirmModal + v-if="deleteModalVisible" + :title="$t('admin_dash.emoji.delete_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="deleteModalVisible = false" + @accepted="deleteEmoji" + > + {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }} + </ConfirmModal> + </template> + </div> + </template> + </Popover> +</template> + +<script> +import Popover from 'components/popover/popover.vue' +import ConfirmModal from 'components/confirm_modal/confirm_modal.vue' +import StillImage from 'components/still-image/still-image.vue' + +export default { + components: { Popover, ConfirmModal, StillImage }, + inject: ['emojiAddr'], + props: { + placement: String, + disabled: { + type: Boolean, + default: false + }, + + newUpload: Boolean, + + title: String, + packName: String, + shortcode: { + type: String, + // Only exists when this is not a new upload + default: '' + }, + file: { + type: String, + // Only exists when this is not a new upload + default: '' + } + }, + emits: ['updatePackFiles', 'displayError'], + data () { + return { + uploadFile: [], + editedShortcode: this.shortcode, + editedFile: this.file, + deleteModalVisible: false + } + }, + computed: { + emojiPreview () { + if (this.newUpload && this.uploadFile.length > 0) { + return URL.createObjectURL(this.uploadFile[0]) + } else if (!this.newUpload) { + return this.emojiAddr(this.file) + } + + return null + }, + isEdited () { + return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file) + } + }, + methods: { + saveEditedEmoji () { + if (!this.isEdited) return + + this.$store.state.api.backendInteractor.updateEmojiFile( + { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false } + ).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return Promise.reject(resp.error) + } + + return resp.json() + }).then(resp => this.$emit('updatePackFiles', resp)) + }, + uploadEmoji () { + this.$store.state.api.backendInteractor.addNewEmojiFile({ + packName: this.packName, + file: this.uploadFile[0], + shortcode: this.editedShortcode, + filename: this.editedFile + }).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + this.$refs.emojiPopover.hidePopover() + + this.editedFile = '' + this.editedShortcode = '' + this.uploadFile = [] + }) + }, + revertEmoji () { + this.editedFile = this.file + this.editedShortcode = this.shortcode + }, + deleteEmoji () { + this.deleteModalVisible = false + + this.$store.state.api.backendInteractor.deleteEmojiFile( + { packName: this.packName, shortcode: this.shortcode } + ).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + }) + } + } +} +</script> + +<style lang="scss"> + .emoji-tab-edit-popover { + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.5em; + + .emoji { + width: 32px; + height: 32px; + } + } +</style> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue @@ -15,7 +15,7 @@ </template> <template #content> <div class="modified-tooltip"> - {{ $t('settings.setting_changed') }} + {{ $t(messageKey) }} </div> </template> </Popover> @@ -33,7 +33,13 @@ library.add( export default { components: { Popover }, - props: ['changed'] + props: { + changed: Boolean, + messageKey: { + type: String, + default: 'settings.setting_changed' + } + } } </script> diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue @@ -15,9 +15,10 @@ </template> <slot v-else /> </label> + {{ ' ' }} <input :id="path" - class="number-input" + class="input number-input" type="number" :step="step || 1" :disabled="shouldBeDisabled" diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js @@ -48,6 +48,10 @@ export default { draftMode: { type: Boolean, default: undefined + }, + timedApplyMode: { + type: Boolean, + default: false } }, inject: { @@ -161,7 +165,11 @@ export default { case 'admin': return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v }) default: - return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) + if (this.timedApplyMode) { + return (k, v) => this.$store.dispatch('setOptionTemporarily', { name: k, value: v }) + } else { + return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) + } } }, defaultState () { @@ -195,7 +203,8 @@ export default { } }, canHardReset () { - return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> ')) + return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths && + this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> ')) }, matchesExpertLevel () { return (this.expert || 0) <= this.$store.state.config.expertLevel > 0 diff --git a/src/components/settings_modal/helpers/size_setting.js b/src/components/settings_modal/helpers/size_setting.js @@ -1,40 +0,0 @@ -import Select from 'src/components/select/select.vue' -import Setting from './setting.js' - -export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'] -export const defaultHorizontalUnits = ['px', 'rem', 'vw'] -export const defaultVerticalUnits = ['px', 'rem', 'vh'] - -export default { - ...Setting, - components: { - ...Setting.components, - Select - }, - props: { - ...Setting.props, - min: Number, - units: { - type: Array, - default: () => allCssUnits - } - }, - computed: { - ...Setting.computed, - stateUnit () { - return this.state.replace(/\d+/, '') - }, - stateValue () { - return this.state.replace(/\D+/, '') - } - }, - methods: { - ...Setting.methods, - updateValue (e) { - this.configSink(this.path, parseInt(e.target.value) + this.stateUnit) - }, - updateUnit (e) { - this.configSink(this.path, this.stateValue + e.target.value) - } - } -} diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/size_setting.vue @@ -1,62 +0,0 @@ -<template> - <span - v-if="matchesExpertLevel" - class="SizeSetting" - > - <label - :for="path" - class="size-label" - > - <slot /> - </label> - <input - :id="path" - class="number-input" - type="number" - step="1" - :disabled="disabled" - :min="min || 0" - :value="stateValue" - @change="updateValue" - > - <Select - :id="path" - :model-value="stateUnit" - :disabled="disabled" - class="css-unit-input" - @change="updateUnit" - > - <option - v-for="option in units" - :key="option" - :value="option" - > - {{ option }} - </option> - </Select> - {{ ' ' }} - <ModifiedIndicator - :changed="isChanged" - :onclick="reset" - /> - </span> -</template> - -<script src="./size_setting.js"></script> - -<style lang="scss"> -.SizeSetting { - .number-input { - max-width: 6.5em; - } - - .css-unit-input, - .css-unit-input select { - margin-left: 0.5em; - width: 4em; - max-width: 4em; - min-width: 4em; - } -} - -</style> diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue @@ -17,7 +17,7 @@ </label> <input :id="path" - class="string-input" + class="input string-input" :disabled="shouldBeDisabled" :value="realDraftMode ? draft : state" @change="update" diff --git a/src/components/settings_modal/helpers/unit_setting.js b/src/components/settings_modal/helpers/unit_setting.js @@ -0,0 +1,64 @@ +import Select from 'src/components/select/select.vue' +import Setting from './setting.js' + +export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'] +export const defaultHorizontalUnits = ['px', 'rem', 'vw'] +export const defaultVerticalUnits = ['px', 'rem', 'vh'] + +export default { + ...Setting, + components: { + ...Setting.components, + Select + }, + props: { + ...Setting.props, + min: Number, + units: { + type: Array, + default: () => allCssUnits + }, + unitSet: { + type: String, + default: 'none' + }, + step: { + type: Number, + default: 1 + }, + resetDefault: { + type: Object, + default: null + } + }, + computed: { + ...Setting.computed, + stateUnit () { + return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : '' + }, + stateValue () { + return typeof this.state === 'string' ? this.state.replace(/[^0-9,.]+/, '') : '' + } + }, + methods: { + ...Setting.methods, + getUnitString (value) { + if (this.unitSet === 'none') return value + return this.$t(['settings', 'units', this.unitSet, value].join('.')) + }, + updateValue (e) { + this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit) + }, + updateUnit (e) { + let value = this.stateValue + const newUnit = e.target.value + if (this.resetDefault) { + const replaceValue = this.resetDefault[newUnit] + if (replaceValue != null) { + value = replaceValue + } + } + this.configSink(this.path, value + newUnit) + } + } +} diff --git a/src/components/settings_modal/helpers/unit_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue @@ -0,0 +1,62 @@ +<template> + <span + v-if="matchesExpertLevel" + class="UnitSetting" + > + <label + :for="path" + class="size-label" + > + <slot /> + </label> + {{ ' ' }} + <input + :id="path" + class="input number-input" + type="number" + :step="step" + :disabled="disabled" + :min="min || 0" + :value="stateValue" + @change="updateValue" + > + <Select + :id="path" + :model-value="stateUnit" + :disabled="disabled" + class="unit-input unstyled" + @change="updateUnit" + > + <option + v-for="option in units" + :key="option" + :value="option" + > + {{ getUnitString(option) }} + </option> + </Select> + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + </span> +</template> + +<script src="./unit_setting.js"></script> + +<style lang="scss"> +.UnitSetting { + .number-input { + max-width: 6.5em; + text-align: right; + } + + .unit-input, + .unit-input select { + min-width: 4em; + width: auto; + } +} + +</style> diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js @@ -4,6 +4,7 @@ import AsyncComponentError from 'src/components/async_component_error/async_comp import getResettableAsyncComponent from 'src/services/resettable_async_component.js' import Popover from '../popover/popover.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { cloneDeep, isEqual } from 'lodash' import { @@ -53,6 +54,7 @@ const SettingsModal = { Modal, Popover, Checkbox, + ConfirmModal, SettingsModalUserContent: getResettableAsyncComponent( () => import('./settings_modal_user_content.vue'), { @@ -165,6 +167,7 @@ const SettingsModal = { }, computed: { currentSaveStateNotice () { + console.log(this.$store.state.interface.settings.currentSaveStateNotice) return this.$store.state.interface.settings.currentSaveStateNotice }, modalActivated () { diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss @@ -1,8 +1,10 @@ -@import "src/variables"; - .settings-modal { overflow: hidden; + h4 { + margin-bottom: 0.5em; + } + .setting-list, .option-list { list-style-type: none; @@ -15,6 +17,14 @@ .suboptions { margin-top: 0.3em; } + + &.two-column { + column-count: 2; + + > li { + break-inside: avoid; + } + } } .setting-description { diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -14,7 +14,7 @@ <div v-if="currentSaveStateNotice" class="alert" - :class="{ transparent: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}" + :class="{ success: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}" @click.prevent > {{ currentSaveStateNotice.error ? $t('settings.saving_err') : $t('settings.saving_ok') }} @@ -70,7 +70,7 @@ <template #content="{close}"> <div class="dropdown-menu"> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="backup" @click="close" > @@ -80,7 +80,7 @@ /><span>{{ $t("settings.file_export_import.backup_settings") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="backupWithTheme" @click="close" > @@ -90,7 +90,7 @@ /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="restore" @click="close" > @@ -147,6 +147,18 @@ </span> </div> </div> + <teleport to="#modal"> + <ConfirmModal + v-if="$store.state.interface.temporaryChangesTimeoutId" + :title="$t('settings.confirm_new_setting')" + :cancel-text="$t('settings.revert')" + :confirm-text="$t('settings.confirm')" + @cancelled="$store.state.interface.temporaryChangesRevert" + @accepted="$store.state.interface.temporaryChangesConfirm" + > + {{ $t('settings.confirm_new_question') }} + </ConfirmModal> + </teleport> </Modal> </template> diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js @@ -3,6 +3,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import InstanceTab from './admin_tabs/instance_tab.vue' import LimitsTab from './admin_tabs/limits_tab.vue' import FrontendsTab from './admin_tabs/frontends_tab.vue' +import EmojiTab from './admin_tabs/emoji_tab.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -33,7 +34,8 @@ const SettingsModalAdminContent = { InstanceTab, LimitsTab, - FrontendsTab + FrontendsTab, + EmojiTab }, computed: { user () { diff --git a/src/components/settings_modal/settings_modal_admin_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss @@ -1,10 +1,8 @@ -@import "src/variables"; - .settings_tab-switcher { height: 100%; .setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); + border-bottom: 2px solid var(--border); margin: 1em 1em 1.4em; padding-bottom: 1.4em; @@ -33,10 +31,6 @@ margin-bottom: 1em; } - select { - min-width: 10em; - } - textarea { width: 100%; max-width: 100%; @@ -45,8 +39,7 @@ .unavailable, .unavailable svg { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; + color: var(--cRed); } } } diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue @@ -60,6 +60,14 @@ > <FrontendsTab /> </div> + + <div + :label="$t('admin_dash.tabs.emoji')" + icon="face-smile-beam" + data-tab-name="emoji" + > + <EmojiTab /> + </div> </tab-switcher> </template> diff --git a/src/components/settings_modal/settings_modal_user_content.js b/src/components/settings_modal/settings_modal_user_content.js @@ -7,6 +7,7 @@ import FilteringTab from './tabs/filtering_tab.vue' import SecurityTab from './tabs/security_tab/security_tab.vue' import ProfileTab from './tabs/profile_tab.vue' import GeneralTab from './tabs/general_tab.vue' +import AppearanceTab from './tabs/appearance_tab.vue' import VersionTab from './tabs/version_tab.vue' import ThemeTab from './tabs/theme_tab/theme_tab.vue' @@ -19,7 +20,8 @@ import { faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore } from '@fortawesome/free-solid-svg-icons' library.add( @@ -30,7 +32,8 @@ library.add( faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore ) const SettingsModalContent = { @@ -44,6 +47,7 @@ const SettingsModalContent = { SecurityTab, ProfileTab, GeneralTab, + AppearanceTab, VersionTab, ThemeTab }, diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss @@ -1,10 +1,8 @@ -@import "src/variables"; - .settings_tab-switcher { height: 100%; .setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); + border-bottom: 2px solid var(--border); margin: 1em 1em 1.4em; padding-bottom: 1.4em; @@ -33,10 +31,6 @@ margin-bottom: 1em; } - select { - min-width: 10em; - } - textarea { width: 100%; max-width: 100%; @@ -45,8 +39,7 @@ .unavailable, .unavailable svg { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; + color: var(--cRed); } } } diff --git a/src/components/settings_modal/settings_modal_user_content.vue b/src/components/settings_modal/settings_modal_user_content.vue @@ -14,6 +14,20 @@ <GeneralTab /> </div> <div + :label="$t('settings.appearance')" + icon="window-restore" + data-tab-name="appearance" + > + <AppearanceTab /> + </div> + <div + :label="$t('settings.theme')" + icon="paint-brush" + data-tab-name="theme" + > + <ThemeTab /> + </div> + <div v-if="isLoggedIn" :label="$t('settings.profile_tab')" icon="user" @@ -23,6 +37,14 @@ </div> <div v-if="isLoggedIn" + :label="$t('settings.notifications')" + icon="bell" + data-tab-name="notifications" + > + <NotificationsTab /> + </div> + <div + v-if="isLoggedIn" :label="$t('settings.security_tab')" icon="lock" data-tab-name="security" @@ -37,19 +59,13 @@ <FilteringTab /> </div> <div - :label="$t('settings.theme')" - icon="paint-brush" - data-tab-name="theme" - > - <ThemeTab /> - </div> - <div v-if="isLoggedIn" - :label="$t('settings.notifications')" - icon="bell" - data-tab-name="notifications" + :label="$t('settings.mutes_and_blocks')" + :fullHeight="true" + icon="eye-slash" + data-tab-name="mutesAndBlocks" > - <NotificationsTab /> + <MutesAndBlocksTab /> </div> <div v-if="isLoggedIn" @@ -60,15 +76,6 @@ <DataImportExportTab /> </div> <div - v-if="isLoggedIn" - :label="$t('settings.mutes_and_blocks')" - :fullHeight="true" - icon="eye-slash" - data-tab-name="mutesAndBlocks" - > - <MutesAndBlocksTab /> - </div> - <div :label="$t('settings.version.title')" icon="info" data-tab-name="version" diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js @@ -0,0 +1,195 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import FloatSetting from '../helpers/float_setting.vue' +import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue' + +import FontControl from 'src/components/font_control/font_control.vue' + +import { normalizeThemeData } from 'src/modules/interface' + +import { + getThemes +} from 'src/services/style_setter/style_setter.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' +import { init } from 'src/services/theme_data/theme_data_3.service.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +import Preview from './theme_tab/theme_preview.vue' + +library.add( + faGlobe +) + +const AppearanceTab = { + data () { + return { + availableStyles: [], + intersectionObserver: null, + thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.third_column_mode_${mode}`) + })), + forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({ + key: mode, + value: i - 1, + label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`) + })), + underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({ + key: mode, + value: mode, + label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`) + })) + } + }, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + FloatSetting, + UnitSetting, + ProfileSettingIndicator, + FontControl, + Preview + }, + mounted () { + getThemes() + .then((promises) => { + return Promise.all( + Object.entries(promises) + .map(([k, v]) => v.then(res => [k, res])) + ) + }) + .then(themes => themes.reduce((acc, [k, v]) => { + if (v) { + return [ + ...acc, + { + name: v.name || v[0], + key: k, + data: v + } + ] + } else { + return acc + } + }, [])) + .then((themesComplete) => { + this.availableStyles = themesComplete + }) + + if (window.IntersectionObserver) { + this.intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(({ target, isIntersecting }) => { + if (!isIntersecting) return + const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey) + this.$nextTick(() => { + if (theme) theme.ready = true + }) + observer.unobserve(target) + }) + }, { + root: this.$refs.themeList + }) + } + }, + updated () { + this.$nextTick(() => { + this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => { + this.intersectionObserver.observe(node) + }) + }) + }, + computed: { + noIntersectionObserver () { + return !window.IntersectionObserver + }, + horizontalUnits () { + return defaultHorizontalUnits + }, + fontsOverride () { + return this.$store.getters.mergedConfig.fontsOverride + }, + columns () { + const mode = this.$store.getters.mergedConfig.thirdColumnMode + + const notif = mode === 'none' ? [] : ['notifs'] + + if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') { + return [...notif, 'content', 'sidebar'] + } else { + return ['sidebar', 'content', ...notif] + } + }, + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, + instanceWallpaperUsed () { + return this.$store.state.instance.background && + !this.$store.state.users.currentUser.background_image + }, + instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, + language: { + get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, + set: function (val) { + this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) + } + }, + isCustomThemeUsed () { + const { theme } = this.mergedConfig + return theme === 'custom' || theme === null + }, + ...SharedComputedObject() + }, + methods: { + updateFont (key, value) { + console.log(key, value) + this.$store.dispatch('setOption', { + name: 'theme3hacks', + value: { + ...this.mergedConfig.theme3hacks, + fonts: { + ...this.mergedConfig.theme3hacks.fonts, + [key]: value + } + } + }) + }, + isThemeActive (key) { + const { theme } = this.mergedConfig + return key === theme + }, + setTheme (name) { + this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true }) + }, + previewTheme (key, input) { + const style = normalizeThemeData(input) + const x = 2 + if (x === 1) return + const theme2 = convertTheme2To3(style) + const theme3 = init({ + inputRuleset: theme2, + ultimateBackgroundColor: '#000000', + liteMode: true, + debug: true, + onlyNormalState: true + }) + + return getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview-' + key + ).join('\n') + } + } +} + +export default AppearanceTab diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue @@ -0,0 +1,313 @@ +<template> + <div class="appearance-tab" :label="$t('settings.general')"> + <div class="setting-item"> + <h2>{{ $t('settings.theme') }}</h2> + <ul + class="theme-list" + ref="themeList" + > + <button + v-if="isCustomThemeUsed" + disabled + class="button-default theme-preview" + > + <preview /> + <h4 class="theme-name">{{ $t('settings.style.custom_theme_used') }}</h4> + </button> + <button + v-for="style in availableStyles" + :data-theme-key="style.key" + :key="style.key" + class="button-default theme-preview" + :class="{ toggled: isThemeActive(style.key) }" + @click="setTheme(style.key)" + > + <!-- eslint-disable vue/no-v-text-v-html-on-component --> + <component + :is="'style'" + v-if="style.ready || noIntersectionObserver" + v-html="previewTheme(style.key, style.data)" + /> + <!-- eslint-enable vue/no-v-text-v-html-on-component --> + <preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/> + <h4 class="theme-name">{{ style.name }}</h4> + </button> + </ul> + </div> + <div class="alert neutral theme-notice"> + {{ $t("settings.style.appearance_tab_note") }} + </div> + <div class="setting-item"> + <h2>{{ $t('settings.scale_and_layout') }}</h2> + <ul class="setting-list"> + <li> + <UnitSetting + path="textSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 14, 'rem': 1 }" + timed-apply-mode + > + {{ $t('settings.text_size') }} + </UnitSetting> + <div> + <small> + <i18n-t + scope="global" + keypath="settings.text_size_tip" + tag="span" + > + <code>px</code> + <code>rem</code> + </i18n-t> + <br/> + <i18n-t + scope="global" + keypath="settings.text_size_tip2" + tag="span" + > + <code>14px</code> + </i18n-t> + </small> + </div> + </li> + <li> + <h3>{{ $t('settings.style.interface_font_user_override') }}</h3> + <ul class="setting-list"> + <li> + <FontControl + :model-value="mergedConfig.theme3hacks.fonts.interface" + name="ui" + :label="$t('settings.style.fonts.components.interface')" + :fallback="{ family: 'sans-serif' }" + no-inherit="1" + @update:modelValue="v => updateFont('interface', v)" + /> + </li> + <li> + <FontControl + v-if="expertLevel > 0" + :model-value="mergedConfig.theme3hacks.fonts.input" + name="input" + :fallback="{ family: 'inherit' }" + :label="$t('settings.style.fonts.components.input')" + @update:modelValue="v => updateFont('input', v)" + /> + </li> + <li> + <FontControl + v-if="expertLevel > 0" + :model-value="mergedConfig.theme3hacks.fonts.post" + name="post" + :fallback="{ family: 'inherit' }" + :label="$t('settings.style.fonts.components.post')" + @update:modelValue="v => updateFont('post', v)" + /> + </li> + <li> + <FontControl + v-if="expertLevel > 0" + :model-value="mergedConfig.theme3hacks.fonts.monospace" + name="postCode" + :fallback="{ family: 'monospace' }" + :label="$t('settings.style.fonts.components.monospace')" + @update:modelValue="v => updateFont('monospace', v)" + /> + </li> + </ul> + </li> + <li> + <UnitSetting + path="emojiSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 32, 'rem': 2.2 }" + > + {{ $t('settings.emoji_size') }} + </UnitSetting> + <ul + class="setting-list suboptions" + > + <li> + <FloatSetting + v-if="user" + path="emojiReactionsScale" + expert="1" + > + {{ $t('settings.emoji_reactions_scale') }} + </FloatSetting> + </li> + </ul> + </li> + <li> + <UnitSetting + path="navbarSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 55, 'rem': 3.5 }" + > + {{ $t('settings.navbar_size') }} + </UnitSetting> + </li> + <h3>{{ $t('settings.columns') }}</h3> + <li> + <UnitSetting + path="panelHeaderSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 52, 'rem': 3.2 }" + timed-apply-mode + > + {{ $t('settings.panel_header_size') }} + </UnitSetting> + </li> + <li> + <BooleanSetting path="sidebarRight"> + {{ $t('settings.right_sidebar') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="navbarColumnStretch"> + {{ $t('settings.navbar_column_stretch') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + v-if="user" + id="thirdColumnMode" + path="thirdColumnMode" + :options="thirdColumnModeOptions" + > + {{ $t('settings.third_column_mode') }} + </ChoiceSetting> + </li> + <li v-if="expertLevel > 0"> + {{ $t('settings.column_sizes') }} + <div class="column-settings"> + <UnitSetting + v-for="column in columns" + :key="column" + :path="column + 'ColumnWidth'" + :units="horizontalUnits" + expert="1" + > + {{ $t('settings.column_sizes_' + column) }} + </UnitSetting> + </div> + </li> + <li> + <BooleanSetting path="disableStickyHeaders"> + {{ $t('settings.disable_sticky_headers') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="showScrollbars"> + {{ $t('settings.show_scrollbars') }} + </BooleanSetting> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.visual_tweaks') }}</h2> + <ul class="setting-list"> + <li> + <ChoiceSetting + id="forcedRoundness" + path="forcedRoundness" + :options="forcedRoundnessOptions" + > + {{ $t('settings.style.themes3.hacks.force_interface_roundness') }} + </ChoiceSetting> + </li> + <li> + <ChoiceSetting + id="underlayOverride" + path="theme3hacks.underlay" + :options="underlayOverrideModes" + > + {{ $t('settings.style.themes3.hacks.underlay_overrides') }} + </ChoiceSetting> + </li> + <li v-if="instanceWallpaperUsed"> + <BooleanSetting path="hideInstanceWallpaper"> + {{ $t('settings.hide_wallpaper') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="forceThemeRecompilation" + :expert="1" + > + {{ $t('settings.force_theme_recompilation_debug') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="themeDebug" + :expert="1" + > + {{ $t('settings.theme_debug') }} + </BooleanSetting> + </li> + </ul> + </div> + </div> +</template> + +<script src="./appearance_tab.js"></script> + +<style lang="scss"> +.appearance-tab { + .theme-notice { + padding: 0.5em; + margin: 1em; + } + + .column-settings { + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + } + + .column-settings .size-label { + display: block; + margin-bottom: 0.5em; + margin-top: 0.5em; + } + + .theme-list { + list-style: none; + display: flex; + flex-wrap: wrap; + margin: -0.5em 0; + height: 25em; + overflow-x: hidden; + overflow-y: auto; + scrollbar-gutter: stable; + border-radius: var(--roundness); + border: 1px solid var(--border); + padding: 0; + + .theme-preview { + font-size: 1rem; // fix for firefox + width: 19rem; + display: flex; + flex-direction: column; + align-items: center; + margin: 0.5em; + + &.placeholder { + opacity: 0.2; + } + + .theme-preview-container { + pointer-events: none; + zoom: 0.5; + border: none; + border-radius: var(--roundness); + text-align: left; + } + } + } +} +</style> diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,6 +1,7 @@ import { filter, trim, debounce } from 'lodash' import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' +import UnitSetting from '../helpers/unit_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' @@ -19,6 +20,7 @@ const FilteringTab = { components: { BooleanSetting, ChoiceSetting, + UnitSetting, IntegerSetting }, computed: { diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -45,13 +45,18 @@ </BooleanSetting> </li> <li> + <BooleanSetting path="muteSensitiveStatuses"> + {{ $t('settings.mute_sensitive_posts') }} + </BooleanSetting> + </li> + <li> <BooleanSetting path="hidePostStats"> {{ $t('settings.hide_post_stats') }} </BooleanSetting> </li> <li> <BooleanSetting path="hideBotIndication"> - {{ $t('settings.hide_bot_indication') }} + {{ $t('settings.hide_actor_type_indication') }} </BooleanSetting> </li> <ChoiceSetting @@ -67,7 +72,7 @@ <textarea id="muteWords" v-model="muteWordsString" - class="resize-height" + class="input resize-height" /> <div>{{ $t('settings.filtering_explanation') }}</div> </li> @@ -91,6 +96,22 @@ {{ $t('settings.hide_attachments_in_convo') }} </BooleanSetting> </li> + <li> + <BooleanSetting path="hideScrobbles"> + {{ $t('settings.hide_scrobbles') }} + </BooleanSetting> + </li> + <li> + <UnitSetting + key="hideScrobblesAfter" + path="hideScrobblesAfter" + :units="['m', 'h', 'd']" + unitSet="time" + expert="1" + > + {{ $t('settings.hide_scrobbles_after') }} + </UnitSetting> + </li> </ul> </div> <div diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js @@ -3,7 +3,7 @@ import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_setting.vue' import FloatSetting from '../helpers/float_setting.vue' -import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue' +import UnitSetting from '../helpers/unit_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' @@ -40,11 +40,6 @@ const GeneralTab = { value: mode, label: this.$t(`settings.mention_link_display_${mode}`) })), - thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({ - key: mode, - value: mode, - label: this.$t(`settings.third_column_mode_${mode}`) - })), userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({ key: mode, value: mode, @@ -64,15 +59,12 @@ const GeneralTab = { ChoiceSetting, IntegerSetting, FloatSetting, - SizeSetting, + UnitSetting, InterfaceLanguageSwitcher, ScopeSelector, ProfileSettingIndicator }, computed: { - horizontalUnits () { - return defaultHorizontalUnits - }, postFormats () { return this.$store.state.instance.postFormats || [] }, @@ -83,23 +75,6 @@ const GeneralTab = { label: this.$t(`post_status.content_type["${format}"]`) })) }, - columns () { - const mode = this.$store.getters.mergedConfig.thirdColumnMode - - const notif = mode === 'none' ? [] : ['notifs'] - - if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') { - return [...notif, 'content', 'sidebar'] - } else { - return ['sidebar', 'content', ...notif] - } - }, - instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, - instanceWallpaperUsed () { - return this.$store.state.instance.background && - !this.$store.state.users.currentUser.background_image - }, - instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, language: { get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, set: function (val) { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -15,11 +15,6 @@ {{ $t('settings.hide_isp') }} </BooleanSetting> </li> - <li v-if="instanceWallpaperUsed"> - <BooleanSetting path="hideInstanceWallpaper"> - {{ $t('settings.hide_wallpaper') }} - </BooleanSetting> - </li> <li> <BooleanSetting path="stopGifs"> {{ $t('settings.stop_gifs') }} @@ -98,53 +93,6 @@ {{ $t('settings.hide_shoutbox') }} </BooleanSetting> </li> - <li> - <h3>{{ $t('settings.columns') }}</h3> - </li> - <li> - <BooleanSetting path="disableStickyHeaders"> - {{ $t('settings.disable_sticky_headers') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="showScrollbars"> - {{ $t('settings.show_scrollbars') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="sidebarRight"> - {{ $t('settings.right_sidebar') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="navbarColumnStretch"> - {{ $t('settings.navbar_column_stretch') }} - </BooleanSetting> - </li> - <li> - <ChoiceSetting - v-if="user" - id="thirdColumnMode" - path="thirdColumnMode" - :options="thirdColumnModeOptions" - > - {{ $t('settings.third_column_mode') }} - </ChoiceSetting> - </li> - <li v-if="expertLevel > 0"> - {{ $t('settings.column_sizes') }} - <div class="column-settings"> - <SizeSetting - v-for="column in columns" - :key="column" - :path="column + 'ColumnWidth'" - :units="horizontalUnits" - expert="1" - > - {{ $t('settings.column_sizes_' + column) }} - </SizeSetting> - </div> - </li> <li class="select-multiple"> <span class="label">{{ $t('settings.confirm_dialogs') }}</span> <ul class="option-list"> @@ -269,15 +217,6 @@ {{ $t('settings.no_rich_text_description') }} </BooleanSetting> </li> - <li> - <FloatSetting - v-if="user" - path="emojiReactionsScale" - expert="1" - > - {{ $t('settings.emoji_reactions_scale') }} - </FloatSetting> - </li> <h3>{{ $t('settings.attachments') }}</h3> <li> <BooleanSetting @@ -520,17 +459,3 @@ </template> <script src="./general_tab.js"></script> - -<style lang="scss"> -.column-settings { - display: flex; - justify-content: space-evenly; - flex-wrap: wrap; -} - -.column-settings .size-label { - display: block; - margin-bottom: 0.5em; - margin-top: 0.5em; -} -</style> diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js @@ -16,6 +16,10 @@ const NotificationsTab = { user () { return this.$store.state.users.currentUser }, + canReceiveReports () { + if (!this.user) { return false } + return this.user.privileges.includes('reports_manage_reports') + }, ...SharedComputedObject() }, methods: { diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue @@ -1,6 +1,34 @@ <template> <div :label="$t('settings.notifications')"> <div class="setting-item"> + <h2>{{ $t('settings.notification_setting_annoyance') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path="closingDrawerMarksAsSeen"> + {{ $t('settings.notification_setting_drawer_marks_as_seen') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="ignoreInactionableSeen"> + {{ $t('settings.notification_setting_ignore_inactionable_seen') }} + </BooleanSetting> + <div> + <small> + {{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }} + </small> + </div> + </li> + <li> + <BooleanSetting + path="unseenAtTop" + expert="1" + > + {{ $t('settings.notification_setting_unseen_at_top') }} + </BooleanSetting> + </li> + </ul> + </div> + <div class="setting-item"> <h2>{{ $t('settings.notification_setting_filters') }}</h2> <ul class="setting-list"> <li> @@ -11,42 +39,201 @@ {{ $t('settings.notification_setting_block_from_strangers') }} </BooleanSetting> </li> - <li class="select-multiple"> - <span class="label">{{ $t('settings.notification_visibility') }}</span> - <ul class="option-list"> + <li> + <h3> {{ $t('settings.notification_visibility') }}</h3> + <p v-if="expertLevel > 0"> + {{ $t('settings.notification_setting_filters_chrome_push') }} + </p> + <ul class="setting-list two-column"> <li> - <BooleanSetting path="notificationVisibility.likes"> - {{ $t('settings.notification_visibility_likes') }} - </BooleanSetting> + <h4> {{ $t('settings.notification_visibility_mentions') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.mentions"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.mentions"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> </li> <li> - <BooleanSetting path="notificationVisibility.repeats"> - {{ $t('settings.notification_visibility_repeats') }} - </BooleanSetting> + <h4> {{ $t('settings.notification_visibility_statuses') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.statuses"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.statuses"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> </li> <li> - <BooleanSetting path="notificationVisibility.follows"> - {{ $t('settings.notification_visibility_follows') }} - </BooleanSetting> + <h4> {{ $t('settings.notification_visibility_likes') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.likes"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.likes"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_repeats') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.repeats"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.repeats"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> </li> <li> - <BooleanSetting path="notificationVisibility.mentions"> - {{ $t('settings.notification_visibility_mentions') }} + <h4> {{ $t('settings.notification_visibility_emoji_reactions') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.emojiReactions"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.emojiReactions"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_follows') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.follows"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.follows"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_follow_requests') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.followRequest"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.followRequest"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_moves') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.moves"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.moves"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_polls') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.polls"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.polls"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li v-if="canReceiveReports"> + <h4> {{ $t('settings.notification_visibility_reports') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.reports"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.reports"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + </ul> + </li> + <li> + <BooleanSetting path="showExtraNotifications"> + {{ $t('settings.notification_show_extra') }} + </BooleanSetting> + </li> + <li> + <ul class="setting-list suboptions"> + <li> + <BooleanSetting + path="showChatsInExtraNotifications" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_chats') }} </BooleanSetting> </li> <li> - <BooleanSetting path="notificationVisibility.moves"> - {{ $t('settings.notification_visibility_moves') }} + <BooleanSetting + path="showAnnouncementsInExtraNotifications" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_announcements') }} </BooleanSetting> </li> <li> - <BooleanSetting path="notificationVisibility.emojiReactions"> - {{ $t('settings.notification_visibility_emoji_reactions') }} + <BooleanSetting + path="showFollowRequestsInExtraNotifications" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_follow_requests') }} </BooleanSetting> </li> <li> - <BooleanSetting path="notificationVisibility.polls"> - {{ $t('settings.notification_visibility_polls') }} + <BooleanSetting + path="showExtraNotificationsTip" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_tip') }} </BooleanSetting> </li> </ul> @@ -67,6 +254,21 @@ > {{ $t('settings.enable_web_push_notifications') }} </BooleanSetting> + <ul class="setting-list suboptions"> + <li> + <BooleanSetting + path="webPushAlwaysShowNotifications" + :disabled="!mergedConfig.webPushNotifications" + > + {{ $t('settings.enable_web_push_always_show') }} + </BooleanSetting> + <div :class="{ faint: !mergedConfig.webPushNotifications }"> + <small> + {{ $t('settings.enable_web_push_always_show_tip') }} + </small> + </div> + </li> + </ul> </li> <li> <BooleanSetting diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js @@ -9,6 +9,7 @@ import suggestor from 'src/components/emoji_input/suggestor.js' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' +import Select from 'src/components/select/select.vue' import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import localeService from 'src/services/locale/locale.service.js' @@ -39,6 +40,7 @@ const ProfileTab = { showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, bot: this.$store.state.users.currentUser.bot, + actorType: this.$store.state.users.currentUser.actor_type, pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, @@ -57,7 +59,8 @@ const ProfileTab = { ProgressButton, Checkbox, BooleanSetting, - InterfaceLanguageSwitcher + InterfaceLanguageSwitcher, + Select }, computed: { user () { @@ -116,6 +119,12 @@ const ProfileTab = { bannerImgSrc () { const src = this.$store.state.users.currentUser.cover_photo return (!src) ? this.defaultBanner : src + }, + groupActorAvailable () { + return this.$store.state.instance.groupActorAvailable + }, + availableActorTypes () { + return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service'] } }, methods: { @@ -127,7 +136,7 @@ const ProfileTab = { /* eslint-disable camelcase */ display_name: this.newName, fields_attributes: this.newFields.filter(el => el != null), - bot: this.bot, + actor_type: this.actorType, show_role: this.showRole, birthday: this.newBirthday || '', show_birthday: this.showBirthday diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss @@ -1,5 +1,3 @@ -@import "../../../variables"; - .profile-tab { .bio { margin: 0; @@ -43,16 +41,14 @@ display: block; width: 100%; height: 100%; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); } .reset-button { position: absolute; top: 0.2em; right: 0.2em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); background-color: rgb(0 0 0 / 60%); opacity: 0.7; width: 1.5em; diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue @@ -12,7 +12,7 @@ <input id="username" v-model="newName" - class="name-changer" + class="input name-changer" v-bind="propsToNative(inputProps)" > </template> @@ -26,7 +26,7 @@ <template #default="inputProps"> <textarea v-model="newBio" - class="bio resize-height" + class="input bio resize-height" v-bind="propsToNative(inputProps)" /> </template> @@ -47,7 +47,7 @@ id="birthday" v-model="newBirthday" type="date" - class="birthday-input" + class="input birthday-input" > <Checkbox v-model="showBirthday"> {{ $t('settings.birthday.show_birthday') }} @@ -71,6 +71,7 @@ v-model="newFields[i].name" :placeholder="$t('settings.profile_fields.name')" v-bind="propsToNative(inputProps)" + class="input" > </template> </EmojiInput> @@ -85,6 +86,7 @@ v-model="newFields[i].value" :placeholder="$t('settings.profile_fields.value')" v-bind="propsToNative(inputProps)" + class="input" > </template> </EmojiInput> @@ -109,10 +111,24 @@ </button> </div> <p> - <Checkbox v-model="bot"> - {{ $t('settings.bot') }} - </Checkbox> + <label> + {{ $t('settings.actor_type') }} + <Select v-model="actorType"> + <option + v-for="option in availableActorTypes" + :key="option" + :value="option" + > + {{ $t('settings.actor_type_' + option) }} + </option> + </Select> + </label> </p> + <div v-if="groupActorAvailable"> + <small> + {{ $t('settings.actor_type_description') }} + </small> + </div> <p> <interface-language-switcher :prompt-text="$t('settings.email_language')" @@ -191,6 +207,7 @@ <div> <input type="file" + class="input" @change="uploadFile('banner', $event)" > </div> @@ -233,6 +250,7 @@ <div> <input type="file" + class="input" @change="uploadFile('background', $event)" > </div> diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue @@ -99,12 +99,14 @@ <input v-model="otpConfirmToken" type="text" + class="input" > <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p> <input v-model="currentPassword" type="password" + class="input" > <div class="confirm-otp-actions"> <button @@ -137,8 +139,6 @@ <script src="./mfa.js"></script> <style lang="scss"> -@import "../../../../variables"; - .mfa-settings { .mfa-heading, .method-item { @@ -149,8 +149,7 @@ } .warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } .setup-otp { diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue @@ -21,16 +21,13 @@ </template> <script src="./mfa_backup_codes.js"></script> <style lang="scss"> -@import "../../../../variables"; - .mfa-backup-codes { .warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } .backup-codes { - font-family: var(--postCodeFont, monospace); + font-family: var(--monoFont); } } </style> diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue @@ -30,6 +30,7 @@ <input v-model="currentPassword" type="password" + class="input" > </confirm> <div diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -8,6 +8,7 @@ v-model="newEmail" type="email" autocomplete="email" + class="input" > </div> <div> @@ -16,6 +17,7 @@ v-model="changeEmailPassword" type="password" autocomplete="current-password" + class="input" > </div> <button @@ -40,6 +42,7 @@ <input v-model="changePasswordInputs[0]" type="password" + class="input" > </div> <div> @@ -47,6 +50,7 @@ <input v-model="changePasswordInputs[1]" type="password" + class="input" > </div> <div> @@ -54,6 +58,7 @@ <input v-model="changePasswordInputs[2]" type="password" + class="input" > </div> <button @@ -155,6 +160,7 @@ </i18n-t> <input v-model="addAliasTarget" + class="input" > </div> <button @@ -187,6 +193,7 @@ </i18n-t> <input v-model="moveAccountTarget" + class="input" > </div> <div> @@ -195,6 +202,7 @@ v-model="moveAccountPassword" type="password" autocomplete="current-password" + class="input" > </div> <button @@ -222,6 +230,7 @@ <input v-model="deleteAccountConfirmPasswordInput" type="password" + class="input" > <button class="btn button-default" diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -1,149 +0,0 @@ -<template> - <div class="preview-container"> - <div class="underlay underlay-preview" /> - <div class="panel dummy"> - <div class="panel-heading"> - <div class="title"> - {{ $t('settings.style.preview.header') }} - <span class="badge badge-notification"> - 99 - </span> - </div> - <span class="faint"> - {{ $t('settings.style.preview.header_faint') }} - </span> - <span class="alert error"> - {{ $t('settings.style.preview.error') }} - </span> - <button class="btn button-default"> - {{ $t('settings.style.preview.button') }} - </button> - </div> - <div class="panel-body theme-preview-content"> - <div class="post"> - <div class="avatar still-image"> - ( ͡° ͜ʖ ͡°) - </div> - <div class="content"> - <h4> - {{ $t('settings.style.preview.content') }} - </h4> - - <i18n-t - scope="global" - keypath="settings.style.preview.text" - > - <code style="font-family: var(--postCodeFont);"> - {{ $t('settings.style.preview.mono') }} - </code> - <a style="color: var(--link);"> - {{ $t('settings.style.preview.link') }} - </a> - </i18n-t> - - <div class="icons"> - <FAIcon - fixed-width - style="color: var(--cBlue);" - class="fa-scale-110 fa-old-padding" - icon="reply" - /> - <FAIcon - fixed-width - style="color: var(--cGreen);" - class="fa-scale-110 fa-old-padding" - icon="retweet" - /> - <FAIcon - fixed-width - style="color: var(--cOrange);" - class="fa-scale-110 fa-old-padding" - icon="star" - /> - <FAIcon - fixed-width - style="color: var(--cRed);" - class="fa-scale-110 fa-old-padding" - icon="times" - /> - </div> - </div> - </div> - - <div class="after-post"> - <div class="avatar-alt"> - :^) - </div> - <div class="content"> - <i18n-t - keypath="settings.style.preview.fine_print" - tag="span" - class="faint" - scope="global" - > - <a style="color: var(--faintLink);"> - {{ $t('settings.style.preview.faint_link') }} - </a> - </i18n-t> - </div> - </div> - <div class="separator" /> - - <span class="alert error"> - {{ $t('settings.style.preview.error') }} - </span> - <input - :value="$t('settings.style.preview.input')" - type="text" - > - - <div class="actions"> - <span class="checkbox"> - <input - id="preview_checkbox" - checked="very yes" - type="checkbox" - > - <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label> - </span> - <button class="btn button-default"> - {{ $t('settings.style.preview.button') }} - </button> - </div> - </div> - </div> - </div> -</template> - -<script> -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faTimes, - faStar, - faRetweet, - faReply -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faTimes, - faStar, - faRetweet, - faReply -) - -export default {} -</script> - -<style lang="scss"> -.preview-container { - position: relative; -} - -.underlay-preview { - position: absolute; - top: 0; - bottom: 0; - left: 10px; - right: 10px; -} -</style> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_preview.vue b/src/components/settings_modal/tabs/theme_tab/theme_preview.vue @@ -0,0 +1,250 @@ +<template> + <div class="theme-preview-container"> + <div class="underlay underlay-preview" /> + <div class="panel dummy"> + <div class="panel-heading"> + <div class="title"> + {{ $t('settings.style.preview.header') }} + <span class="badge -notification"> + 99 + </span> + </div> + <span class="faint"> + {{ $t('settings.style.preview.header_faint') }} + </span> + <span class="alert error"> + {{ $t('settings.style.preview.error') }} + </span> + <button class="btn button-default"> + {{ $t('settings.style.preview.button') }} + </button> + </div> + <div class="panel-body theme-preview-content"> + <div class="post"> + <div class="avatar still-image"> + ( ͡° ͜ʖ ͡°) + </div> + <div class="content"> + <h4> + {{ $t('settings.style.preview.content') }} + </h4> + + <i18n-t + scope="global" + keypath="settings.style.preview.text" + > + <code style="font-family: var(--postCodeFont);"> + {{ $t('settings.style.preview.mono') }} + </code> + <a style="color: var(--link);"> + {{ $t('settings.style.preview.link') }} + </a> + </i18n-t> + + <div class="icons"> + <FAIcon + fixed-width + style="color: var(--cBlue);" + class="fa-scale-110 fa-old-padding" + icon="reply" + /> + <FAIcon + fixed-width + style="color: var(--cGreen);" + class="fa-scale-110 fa-old-padding" + icon="retweet" + /> + <FAIcon + fixed-width + style="color: var(--cOrange);" + class="fa-scale-110 fa-old-padding" + icon="star" + /> + <FAIcon + fixed-width + style="color: var(--cRed);" + class="fa-scale-110 fa-old-padding" + icon="times" + /> + </div> + </div> + </div> + + <div class="after-post"> + <div class="avatar-alt"> + :^) + </div> + <div class="content"> + <i18n-t + keypath="settings.style.preview.fine_print" + tag="span" + class="faint" + scope="global" + > + <a style="color: var(--linkFaint);"> + {{ $t('settings.style.preview.faint_link') }} + </a> + </i18n-t> + </div> + </div> + <div class="separator" /> + + <span class="alert error"> + {{ $t('settings.style.preview.error') }} + </span> + <input + :value="$t('settings.style.preview.input')" + type="text" + class="input" + > + + <div class="actions"> + <Checkbox> + {{ $t('settings.style.preview.checkbox') }} + </Checkbox> + <button class="btn button-default"> + {{ $t('settings.style.preview.button') }} + </button> + </div> + </div> + </div> + </div> +</template> + +<script> +import Checkbox from 'src/components/checkbox/checkbox.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faStar, + faRetweet, + faReply +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faStar, + faRetweet, + faReply +) + +export default { + components: { + Checkbox + } +} +</script> + +<style lang="scss"> +.theme-preview-container { + position: relative; + border-top: 1px dashed; + border-bottom: 1px dashed; + border-color: var(--border); + margin: 1em 0; + padding: 1em; + background-color: var(--wallpaper); + background-image: var(--body-background-image); + background-size: cover; + background-position: 50% 50%; + + .theme-preview-content { + padding: 20px; + } + + .dummy { + .post { + font-family: var(--postFont); + display: flex; + + .content { + flex: 1; + + h4 { + margin-bottom: 0.25em; + } + + .icons { + margin-top: 0.5em; + display: flex; + + i { + margin-right: 1em; + } + } + } + } + + .after-post { + margin-top: 1em; + display: flex; + align-items: center; + } + + .avatar, + .avatar-alt { + background: + linear-gradient( + 135deg, + #b8e1fc 0%, + #a9d2f3 10%, + #90bae4 25%, + #90bcea 37%, + #90bff0 50%, + #6ba8e5 51%, + #a2daf5 83%, + #bdf3fd 100% + ); + color: black; + font-family: sans-serif; + text-align: center; + margin-right: 1em; + } + + .avatar-alt { + flex: 0 auto; + margin-left: 28px; + font-size: 12px; + min-width: 20px; + min-height: 20px; + line-height: 20px; + } + + .avatar { + flex: 0 auto; + width: 48px; + height: 48px; + font-size: 14px; + line-height: 48px; + } + + .actions { + display: flex; + align-items: baseline; + + .checkbox { + margin-right: 1em; + flex: 1; + } + } + + .separator { + margin: 1em; + border-bottom: 1px solid; + border-color: var(--border); + } + + .btn { + min-width: 3em; + } + } + + .underlay-preview { + position: absolute; + top: 0; + bottom: 0; + left: 10px; + right: 10px; + } +} + </style> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -1,18 +1,11 @@ import { rgb2hex, hex2rgb, - getContrastRatioLayers + getContrastRatioLayers, + relativeLuminance } from 'src/services/color_convert/color_convert.js' import { - DEFAULT_SHADOWS, - generateColors, - generateShadows, - generateRadii, - generateFonts, - composePreset, - getThemes, - shadows2to3, - colors2to3 + getThemes } from 'src/services/style_setter/style_setter.js' import { newImporter, @@ -25,8 +18,23 @@ import { CURRENT_VERSION, OPACITIES, getLayers, - getOpacitySlot + getOpacitySlot, + DEFAULT_SHADOWS, + generateColors, + generateShadows, + generateRadii, + generateFonts, + shadows2to3, + colors2to3 } from 'src/services/theme_data/theme_data.service.js' + +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' +import { init } from 'src/services/theme_data/theme_data_3.service.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' + import ColorInput from 'src/components/color_input/color_input.vue' import RangeInput from 'src/components/range_input/range_input.vue' import OpacityInput from 'src/components/opacity_input/opacity_input.vue' @@ -37,7 +45,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import Checkbox from 'src/components/checkbox/checkbox.vue' import Select from 'src/components/select/select.vue' -import Preview from './preview.vue' +import Preview from './theme_preview.vue' // List of color values used in v1 const v1OnlyNames = [ @@ -62,6 +70,7 @@ const colorConvert = (color) => { export default { data () { return { + themeV3Preview: [], themeImporter: newImporter({ validator: this.importValidator, onImport: this.onImport, @@ -78,10 +87,7 @@ export default { tempImportFile: undefined, engineVersion: 0, - previewShadows: {}, - previewColors: {}, - previewRadii: {}, - previewFonts: {}, + previewTheme: {}, shadowsInvalid: true, colorsInvalid: true, @@ -232,13 +238,6 @@ export default { chatMessage: this.chatMessageRadiusLocal } }, - preview () { - return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts) - }, - previewTheme () { - if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} } - return this.preview.theme - }, // This needs optimization maybe previewContrast () { try { @@ -306,14 +305,6 @@ export default { return {} } }, - previewRules () { - if (!this.preview.rules) return '' - return [ - ...Object.values(this.preview.rules), - 'color: var(--text)', - 'font-family: var(--interfaceFont, sans-serif)' - ].join(';') - }, shadowsAvailable () { return Object.keys(DEFAULT_SHADOWS).sort() }, @@ -511,16 +502,15 @@ export default { } }, setCustomTheme () { - this.$store.dispatch('setOption', { - name: 'customTheme', - value: { + this.$store.dispatch('setThemeV2', { + customTheme: { + ignore: true, + themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, ...this.previewTheme - } - }) - this.$store.dispatch('setOption', { - name: 'customThemeSource', - value: { + }, + customThemeSource: { + themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, shadows: this.shadowsLocal, fonts: this.fontsLocal, @@ -530,16 +520,24 @@ export default { } }) }, - updatePreviewColorsAndShadows () { - this.previewColors = generateColors({ + updatePreviewColors () { + const result = generateColors({ opacity: this.currentOpacity, colors: this.currentColors }) - this.previewShadows = generateShadows( - { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion }, - this.previewColors.theme.colors, - this.previewColors.mod - ) + this.previewTheme.colors = result.theme.colors + this.previewTheme.opacity = result.theme.opacity + }, + updatePreviewShadows () { + this.previewTheme.shadows = generateShadows( + { + shadows: this.shadowsLocal, + opacity: this.previewTheme.opacity, + themeEngineVersion: this.engineVersion + }, + this.previewTheme.colors, + relativeLuminance(this.previewTheme.colors.bg) < 0.5 ? 1 : -1 + ).theme.shadows }, importTheme () { this.themeImporter.importData() }, exportTheme () { this.themeExporter.exportData() }, @@ -608,7 +606,7 @@ export default { normalizeLocalState (theme, version = 0, source, forceSource = false) { let input if (typeof source !== 'undefined') { - if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { + if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) { input = source version = source.themeEngineVersion } else { @@ -690,6 +688,8 @@ export default { } else { this.shadowsLocal = shadows } + this.updatePreviewColors() + this.updatePreviewShadows() this.shadowSelected = this.shadowsAvailable[0] } @@ -697,12 +697,25 @@ export default { this.clearFonts() this.fontsLocal = fonts } + }, + updateTheme3Preview () { + const theme2 = convertTheme2To3(this.previewTheme) + const theme3 = init({ + inputRuleset: theme2, + ultimateBackgroundColor: '#000000', + liteMode: true + }) + + this.themeV3Preview = getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview' + ).join('\n') } }, watch: { currentRadii () { try { - this.previewRadii = generateRadii({ radii: this.currentRadii }) + this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii this.radiiInvalid = false } catch (e) { this.radiiInvalid = true @@ -711,9 +724,8 @@ export default { }, shadowsLocal: { handler () { - if (Object.getOwnPropertyNames(this.previewColors).length === 1) return try { - this.updatePreviewColorsAndShadows() + this.updatePreviewShadows() this.shadowsInvalid = false } catch (e) { this.shadowsInvalid = true @@ -725,7 +737,7 @@ export default { fontsLocal: { handler () { try { - this.previewFonts = generateFonts({ fonts: this.fontsLocal }) + this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts this.fontsInvalid = false } catch (e) { this.fontsInvalid = true @@ -736,18 +748,16 @@ export default { }, currentColors () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() this.colorsInvalid = false - this.shadowsInvalid = false } catch (e) { this.colorsInvalid = true - this.shadowsInvalid = true console.warn(e) } }, currentOpacity () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() } catch (e) { console.warn(e) } @@ -755,7 +765,6 @@ export default { selected () { this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => { if (Array.isArray(s)) { - console.log(s[0] === this.selected, this.selected) return s[0] === this.selected } else { return s.name === this.selected diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -1,6 +1,9 @@ -@import "src/variables"; - .theme-tab { + .deprecation-warning { + padding: 0.5em; + margin: 2em; + } + padding-bottom: 2em; .preset-switcher { @@ -12,6 +15,10 @@ margin-right: 0.25em; } + .btn-group .btn { + margin: 0; + } + .style-control { display: flex; align-items: baseline; @@ -159,111 +166,6 @@ } } - .preview-container { - border-top: 1px dashed; - border-bottom: 1px dashed; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - margin: 1em 0; - padding: 1em; - background-color: var(--wallpaper); - background-image: var(--body-background-image); - background-size: cover; - background-position: 50% 50%; - - .dummy { - .post { - font-family: var(--postFont); - display: flex; - - .content { - flex: 1; - - h4 { - margin-bottom: 0.25em; - } - - .icons { - margin-top: 0.5em; - display: flex; - - i { - margin-right: 1em; - } - } - } - } - - .after-post { - margin-top: 1em; - display: flex; - align-items: center; - } - - .avatar, - .avatar-alt { - background: - linear-gradient( - 135deg, - #b8e1fc 0%, - #a9d2f3 10%, - #90bae4 25%, - #90bcea 37%, - #90bff0 50%, - #6ba8e5 51%, - #a2daf5 83%, - #bdf3fd 100% - ); - color: black; - font-family: sans-serif; - text-align: center; - margin-right: 1em; - } - - .avatar-alt { - flex: 0 auto; - margin-left: 28px; - font-size: 12px; - min-width: 20px; - min-height: 20px; - line-height: 20px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } - - .avatar { - flex: 0 auto; - width: 48px; - height: 48px; - font-size: 14px; - line-height: 48px; - } - - .actions { - display: flex; - align-items: baseline; - - .checkbox { - display: inline-flex; - align-items: baseline; - margin-right: 1em; - flex: 1; - } - } - - .separator { - margin: 1em; - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - .btn { - min-width: 3em; - } - } - } - .radius-item { flex-basis: auto; } @@ -296,7 +198,7 @@ border: 0; box-shadow: none; background: transparent; - color: var(--faint, $fallback--faint); + color: var(--textFaint); align-self: stretch; } @@ -316,10 +218,6 @@ max-width: 50em; } - .theme-preview-content { - padding: 20px; - } - .theme-warning { display: flex; align-items: baseline; diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -1,5 +1,8 @@ <template> <div class="theme-tab"> + <div class="alert warning deprecation-warning"> + {{ $t("settings.style.themes2_outdated") }} + </div> <div class="presets-container"> <div class="save-load"> <div @@ -120,7 +123,19 @@ </div> </div> - <preview :style="previewRules" /> + <!-- eslint-disable vue/no-v-text-v-html-on-component --> + <component :is="'style'" v-html="themeV3Preview"/> + <!-- eslint-enable vue/no-v-text-v-html-on-component --> + <preview id="theme-preview"/> + + <div> + <button + class="btn button-default" + @click="updateTheme3Preview" + > + {{ $t("settings.style.update_preview") }} + </button> + </div> <keep-alive> <tab-switcher key="style-tweak"> @@ -156,7 +171,7 @@ <OpacityInput v-model="bgOpacityLocal" name="bgOpacity" - :fallback="previewTheme.opacity.bg" + :fallback="previewTheme.opacity?.bg" /> <ColorInput v-model="textColorLocal" @@ -167,14 +182,14 @@ <ColorInput v-model="accentColorLocal" name="accentColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.accent')" :show-optional-tickbox="typeof linkColorLocal !== 'undefined'" /> <ColorInput v-model="linkColorLocal" name="linkColor" - :fallback="previewTheme.colors.accent" + :fallback="previewTheme.colors?.accent" :label="$t('settings.links')" :show-optional-tickbox="typeof accentColorLocal !== 'undefined'" /> @@ -190,13 +205,13 @@ v-model="fgTextColorLocal" name="fgTextColor" :label="$t('settings.text')" - :fallback="previewTheme.colors.fgText" + :fallback="previewTheme.colors?.fgText" /> <ColorInput v-model="fgLinkColorLocal" name="fgLinkColor" :label="$t('settings.links')" - :fallback="previewTheme.colors.fgLink" + :fallback="previewTheme.colors?.fgLink" /> <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p> </div> @@ -256,14 +271,14 @@ <ColorInput v-model="postLinkColorLocal" name="postLinkColor" - :fallback="previewTheme.colors.accent" + :fallback="previewTheme.colors?.accent" :label="$t('settings.links')" /> <ContrastRatio :contrast="previewContrast.postLink" /> <ColorInput v-model="postGreentextColorLocal" name="postGreentextColor" - :fallback="previewTheme.colors.cGreen" + :fallback="previewTheme.colors?.cGreen" :label="$t('settings.greentext')" /> <ContrastRatio :contrast="previewContrast.postGreentext" /> @@ -272,13 +287,13 @@ v-model="alertErrorColorLocal" name="alertError" :label="$t('settings.style.advanced_colors.alert_error')" - :fallback="previewTheme.colors.alertError" + :fallback="previewTheme.colors?.alertError" /> <ColorInput v-model="alertErrorTextColorLocal" name="alertErrorText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertErrorText" + :fallback="previewTheme.colors?.alertErrorText" /> <ContrastRatio :contrast="previewContrast.alertErrorText" @@ -288,13 +303,13 @@ v-model="alertWarningColorLocal" name="alertWarning" :label="$t('settings.style.advanced_colors.alert_warning')" - :fallback="previewTheme.colors.alertWarning" + :fallback="previewTheme.colors?.alertWarning" /> <ColorInput v-model="alertWarningTextColorLocal" name="alertWarningText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertWarningText" + :fallback="previewTheme.colors?.alertWarningText" /> <ContrastRatio :contrast="previewContrast.alertWarningText" @@ -304,13 +319,13 @@ v-model="alertNeutralColorLocal" name="alertNeutral" :label="$t('settings.style.advanced_colors.alert_neutral')" - :fallback="previewTheme.colors.alertNeutral" + :fallback="previewTheme.colors?.alertNeutral" /> <ColorInput v-model="alertNeutralTextColorLocal" name="alertNeutralText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertNeutralText" + :fallback="previewTheme.colors?.alertNeutralText" /> <ContrastRatio :contrast="previewContrast.alertNeutralText" @@ -319,7 +334,7 @@ <OpacityInput v-model="alertOpacityLocal" name="alertOpacity" - :fallback="previewTheme.opacity.alert" + :fallback="previewTheme.opacity?.alert" /> </div> <div class="color-item"> @@ -328,13 +343,13 @@ v-model="badgeNotificationColorLocal" name="badgeNotification" :label="$t('settings.style.advanced_colors.badge_notification')" - :fallback="previewTheme.colors.badgeNotification" + :fallback="previewTheme.colors?.badgeNotification" /> <ColorInput v-model="badgeNotificationTextColorLocal" name="badgeNotificationText" :label="$t('settings.text')" - :fallback="previewTheme.colors.badgeNotificationText" + :fallback="previewTheme.colors?.badgeNotificationText" /> <ContrastRatio :contrast="previewContrast.badgeNotificationText" @@ -346,19 +361,19 @@ <ColorInput v-model="panelColorLocal" name="panelColor" - :fallback="previewTheme.colors.panel" + :fallback="previewTheme.colors?.panel" :label="$t('settings.background')" /> <OpacityInput v-model="panelOpacityLocal" name="panelOpacity" - :fallback="previewTheme.opacity.panel" + :fallback="previewTheme.opacity?.panel" :disabled="panelColorLocal === 'transparent'" /> <ColorInput v-model="panelTextColorLocal" name="panelTextColor" - :fallback="previewTheme.colors.panelText" + :fallback="previewTheme.colors?.panelText" :label="$t('settings.text')" /> <ContrastRatio @@ -368,7 +383,7 @@ <ColorInput v-model="panelLinkColorLocal" name="panelLinkColor" - :fallback="previewTheme.colors.panelLink" + :fallback="previewTheme.colors?.panelLink" :label="$t('settings.links')" /> <ContrastRatio @@ -381,20 +396,20 @@ <ColorInput v-model="topBarColorLocal" name="topBarColor" - :fallback="previewTheme.colors.topBar" + :fallback="previewTheme.colors?.topBar" :label="$t('settings.background')" /> <ColorInput v-model="topBarTextColorLocal" name="topBarTextColor" - :fallback="previewTheme.colors.topBarText" + :fallback="previewTheme.colors?.topBarText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.topBarText" /> <ColorInput v-model="topBarLinkColorLocal" name="topBarLinkColor" - :fallback="previewTheme.colors.topBarLink" + :fallback="previewTheme.colors?.topBarLink" :label="$t('settings.links')" /> <ContrastRatio :contrast="previewContrast.topBarLink" /> @@ -404,19 +419,19 @@ <ColorInput v-model="inputColorLocal" name="inputColor" - :fallback="previewTheme.colors.input" + :fallback="previewTheme.colors?.input" :label="$t('settings.background')" /> <OpacityInput v-model="inputOpacityLocal" name="inputOpacity" - :fallback="previewTheme.opacity.input" + :fallback="previewTheme.opacity?.input" :disabled="inputColorLocal === 'transparent'" /> <ColorInput v-model="inputTextColorLocal" name="inputTextColor" - :fallback="previewTheme.colors.inputText" + :fallback="previewTheme.colors?.inputText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.inputText" /> @@ -426,33 +441,33 @@ <ColorInput v-model="btnColorLocal" name="btnColor" - :fallback="previewTheme.colors.btn" + :fallback="previewTheme.colors?.btn" :label="$t('settings.background')" /> <OpacityInput v-model="btnOpacityLocal" name="btnOpacity" - :fallback="previewTheme.opacity.btn" + :fallback="previewTheme.opacity?.btn" :disabled="btnColorLocal === 'transparent'" /> <ColorInput v-model="btnTextColorLocal" name="btnTextColor" - :fallback="previewTheme.colors.btnText" + :fallback="previewTheme.colors?.btnText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnText" /> <ColorInput v-model="btnPanelTextColorLocal" name="btnPanelTextColor" - :fallback="previewTheme.colors.btnPanelText" + :fallback="previewTheme.colors?.btnPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnPanelText" /> <ColorInput v-model="btnTopBarTextColorLocal" name="btnTopBarTextColor" - :fallback="previewTheme.colors.btnTopBarText" + :fallback="previewTheme.colors?.btnTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnTopBarText" /> @@ -460,27 +475,27 @@ <ColorInput v-model="btnPressedColorLocal" name="btnPressedColor" - :fallback="previewTheme.colors.btnPressed" + :fallback="previewTheme.colors?.btnPressed" :label="$t('settings.background')" /> <ColorInput v-model="btnPressedTextColorLocal" name="btnPressedTextColor" - :fallback="previewTheme.colors.btnPressedText" + :fallback="previewTheme.colors?.btnPressedText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnPressedText" /> <ColorInput v-model="btnPressedPanelTextColorLocal" name="btnPressedPanelTextColor" - :fallback="previewTheme.colors.btnPressedPanelText" + :fallback="previewTheme.colors?.btnPressedPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnPressedPanelText" /> <ColorInput v-model="btnPressedTopBarTextColorLocal" name="btnPressedTopBarTextColor" - :fallback="previewTheme.colors.btnPressedTopBarText" + :fallback="previewTheme.colors?.btnPressedTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" /> @@ -488,52 +503,52 @@ <ColorInput v-model="btnDisabledColorLocal" name="btnDisabledColor" - :fallback="previewTheme.colors.btnDisabled" + :fallback="previewTheme.colors?.btnDisabled" :label="$t('settings.background')" /> <ColorInput v-model="btnDisabledTextColorLocal" name="btnDisabledTextColor" - :fallback="previewTheme.colors.btnDisabledText" + :fallback="previewTheme.colors?.btnDisabledText" :label="$t('settings.text')" /> <ColorInput v-model="btnDisabledPanelTextColorLocal" name="btnDisabledPanelTextColor" - :fallback="previewTheme.colors.btnDisabledPanelText" + :fallback="previewTheme.colors?.btnDisabledPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ColorInput v-model="btnDisabledTopBarTextColorLocal" name="btnDisabledTopBarTextColor" - :fallback="previewTheme.colors.btnDisabledTopBarText" + :fallback="previewTheme.colors?.btnDisabledTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5> <ColorInput v-model="btnToggledColorLocal" name="btnToggledColor" - :fallback="previewTheme.colors.btnToggled" + :fallback="previewTheme.colors?.btnToggled" :label="$t('settings.background')" /> <ColorInput v-model="btnToggledTextColorLocal" name="btnToggledTextColor" - :fallback="previewTheme.colors.btnToggledText" + :fallback="previewTheme.colors?.btnToggledText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnToggledText" /> <ColorInput v-model="btnToggledPanelTextColorLocal" name="btnToggledPanelTextColor" - :fallback="previewTheme.colors.btnToggledPanelText" + :fallback="previewTheme.colors?.btnToggledPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnToggledPanelText" /> <ColorInput v-model="btnToggledTopBarTextColorLocal" name="btnToggledTopBarTextColor" - :fallback="previewTheme.colors.btnToggledTopBarText" + :fallback="previewTheme.colors?.btnToggledTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" /> @@ -543,20 +558,20 @@ <ColorInput v-model="tabColorLocal" name="tabColor" - :fallback="previewTheme.colors.tab" + :fallback="previewTheme.colors?.tab" :label="$t('settings.background')" /> <ColorInput v-model="tabTextColorLocal" name="tabTextColor" - :fallback="previewTheme.colors.tabText" + :fallback="previewTheme.colors?.tabText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.tabText" /> <ColorInput v-model="tabActiveTextColorLocal" name="tabActiveTextColor" - :fallback="previewTheme.colors.tabActiveText" + :fallback="previewTheme.colors?.tabActiveText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.tabActiveText" /> @@ -566,13 +581,13 @@ <ColorInput v-model="borderColorLocal" name="borderColor" - :fallback="previewTheme.colors.border" + :fallback="previewTheme.colors?.border" :label="$t('settings.style.common.color')" /> <OpacityInput v-model="borderOpacityLocal" name="borderOpacity" - :fallback="previewTheme.opacity.border" + :fallback="previewTheme.opacity?.border" :disabled="borderColorLocal === 'transparent'" /> </div> @@ -581,25 +596,25 @@ <ColorInput v-model="faintColorLocal" name="faintColor" - :fallback="previewTheme.colors.faint" + :fallback="previewTheme.colors?.faint" :label="$t('settings.text')" /> <ColorInput v-model="faintLinkColorLocal" name="faintLinkColor" - :fallback="previewTheme.colors.faintLink" + :fallback="previewTheme.colors?.faintLink" :label="$t('settings.links')" /> <ColorInput v-model="panelFaintColorLocal" name="panelFaintColor" - :fallback="previewTheme.colors.panelFaint" + :fallback="previewTheme.colors?.panelFaint" :label="$t('settings.style.advanced_colors.panel_header')" /> <OpacityInput v-model="faintOpacityLocal" name="faintOpacity" - :fallback="previewTheme.opacity.faint" + :fallback="previewTheme.opacity?.faint" /> </div> <div class="color-item"> @@ -608,12 +623,12 @@ v-model="underlayColorLocal" name="underlay" :label="$t('settings.style.advanced_colors.underlay')" - :fallback="previewTheme.colors.underlay" + :fallback="previewTheme.colors?.underlay" /> <OpacityInput v-model="underlayOpacityLocal" name="underlayOpacity" - :fallback="previewTheme.opacity.underlay" + :fallback="previewTheme.opacity?.underlay" :disabled="underlayOpacityLocal === 'transparent'" /> </div> @@ -623,7 +638,7 @@ v-model="wallpaperColorLocal" name="wallpaper" :label="$t('settings.style.advanced_colors.wallpaper')" - :fallback="previewTheme.colors.wallpaper" + :fallback="previewTheme.colors?.wallpaper" /> </div> <div class="color-item"> @@ -632,13 +647,13 @@ v-model="pollColorLocal" name="poll" :label="$t('settings.background')" - :fallback="previewTheme.colors.poll" + :fallback="previewTheme.colors?.poll" /> <ColorInput v-model="pollTextColorLocal" name="pollText" :label="$t('settings.text')" - :fallback="previewTheme.colors.pollText" + :fallback="previewTheme.colors?.pollText" /> </div> <div class="color-item"> @@ -647,7 +662,7 @@ v-model="iconColorLocal" name="icon" :label="$t('settings.style.advanced_colors.icons')" - :fallback="previewTheme.colors.icon" + :fallback="previewTheme.colors?.icon" /> </div> <div class="color-item"> @@ -656,20 +671,20 @@ v-model="highlightColorLocal" name="highlight" :label="$t('settings.background')" - :fallback="previewTheme.colors.highlight" + :fallback="previewTheme.colors?.highlight" /> <ColorInput v-model="highlightTextColorLocal" name="highlightText" :label="$t('settings.text')" - :fallback="previewTheme.colors.highlightText" + :fallback="previewTheme.colors?.highlightText" /> <ContrastRatio :contrast="previewContrast.highlightText" /> <ColorInput v-model="highlightLinkColorLocal" name="highlightLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.highlightLink" + :fallback="previewTheme.colors?.highlightLink" /> <ContrastRatio :contrast="previewContrast.highlightLink" /> </div> @@ -679,26 +694,26 @@ v-model="popoverColorLocal" name="popover" :label="$t('settings.background')" - :fallback="previewTheme.colors.popover" + :fallback="previewTheme.colors?.popover" /> <OpacityInput v-model="popoverOpacityLocal" name="popoverOpacity" - :fallback="previewTheme.opacity.popover" + :fallback="previewTheme.opacity?.popover" :disabled="popoverOpacityLocal === 'transparent'" /> <ColorInput v-model="popoverTextColorLocal" name="popoverText" :label="$t('settings.text')" - :fallback="previewTheme.colors.popoverText" + :fallback="previewTheme.colors?.popoverText" /> <ContrastRatio :contrast="previewContrast.popoverText" /> <ColorInput v-model="popoverLinkColorLocal" name="popoverLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.popoverLink" + :fallback="previewTheme.colors?.popoverLink" /> <ContrastRatio :contrast="previewContrast.popoverLink" /> </div> @@ -708,20 +723,20 @@ v-model="selectedPostColorLocal" name="selectedPost" :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedPost" + :fallback="previewTheme.colors?.selectedPost" /> <ColorInput v-model="selectedPostTextColorLocal" name="selectedPostText" :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedPostText" + :fallback="previewTheme.colors?.selectedPostText" /> <ContrastRatio :contrast="previewContrast.selectedPostText" /> <ColorInput v-model="selectedPostLinkColorLocal" name="selectedPostLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedPostLink" + :fallback="previewTheme.colors?.selectedPostLink" /> <ContrastRatio :contrast="previewContrast.selectedPostLink" /> </div> @@ -731,20 +746,20 @@ v-model="selectedMenuColorLocal" name="selectedMenu" :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedMenu" + :fallback="previewTheme.colors?.selectedMenu" /> <ColorInput v-model="selectedMenuTextColorLocal" name="selectedMenuText" :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedMenuText" + :fallback="previewTheme.colors?.selectedMenuText" /> <ContrastRatio :contrast="previewContrast.selectedMenuText" /> <ColorInput v-model="selectedMenuLinkColorLocal" name="selectedMenuLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedMenuLink" + :fallback="previewTheme.colors?.selectedMenuLink" /> <ContrastRatio :contrast="previewContrast.selectedMenuLink" /> </div> @@ -753,57 +768,57 @@ <ColorInput v-model="chatBgColorLocal" name="chatBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5> <ColorInput v-model="chatMessageIncomingBgColorLocal" name="chatMessageIncomingBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageIncomingTextColorLocal" name="chatMessageIncomingTextColor" - :fallback="previewTheme.colors.text" + :fallback="previewTheme.colors?.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageIncomingLinkColorLocal" name="chatMessageIncomingLinkColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageIncomingBorderColorLocal" name="chatMessageIncomingBorderLinkColor" - :fallback="previewTheme.colors.fg" + :fallback="previewTheme.colors?.fg" :label="$t('settings.style.advanced_colors.chat.border')" /> <h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5> <ColorInput v-model="chatMessageOutgoingBgColorLocal" name="chatMessageOutgoingBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageOutgoingTextColorLocal" name="chatMessageOutgoingTextColor" - :fallback="previewTheme.colors.text" + :fallback="previewTheme.colors?.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageOutgoingLinkColorLocal" name="chatMessageOutgoingLinkColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageOutgoingBorderColorLocal" name="chatMessageOutgoingBorderLinkColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.style.advanced_colors.chat.border')" /> </div> @@ -826,7 +841,7 @@ v-model="btnRadiusLocal" name="btnRadius" :label="$t('settings.btnRadius')" - :fallback="previewTheme.radii.btn" + :fallback="previewTheme.radii?.btn" max="16" hard-min="0" /> @@ -834,7 +849,7 @@ v-model="inputRadiusLocal" name="inputRadius" :label="$t('settings.inputRadius')" - :fallback="previewTheme.radii.input" + :fallback="previewTheme.radii?.input" max="9" hard-min="0" /> @@ -842,7 +857,7 @@ v-model="checkboxRadiusLocal" name="checkboxRadius" :label="$t('settings.checkboxRadius')" - :fallback="previewTheme.radii.checkbox" + :fallback="previewTheme.radii?.checkbox" max="16" hard-min="0" /> @@ -850,7 +865,7 @@ v-model="panelRadiusLocal" name="panelRadius" :label="$t('settings.panelRadius')" - :fallback="previewTheme.radii.panel" + :fallback="previewTheme.radii?.panel" max="50" hard-min="0" /> @@ -858,7 +873,7 @@ v-model="avatarRadiusLocal" name="avatarRadius" :label="$t('settings.avatarRadius')" - :fallback="previewTheme.radii.avatar" + :fallback="previewTheme.radii?.avatar" max="28" hard-min="0" /> @@ -866,7 +881,7 @@ v-model="avatarAltRadiusLocal" name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" - :fallback="previewTheme.radii.avatarAlt" + :fallback="previewTheme.radii?.avatarAlt" max="28" hard-min="0" /> @@ -874,7 +889,7 @@ v-model="attachmentRadiusLocal" name="attachmentRadius" :label="$t('settings.attachmentRadius')" - :fallback="previewTheme.radii.attachment" + :fallback="previewTheme.radii?.attachment" max="50" hard-min="0" /> @@ -882,7 +897,7 @@ v-model="tooltipRadiusLocal" name="tooltipRadius" :label="$t('settings.tooltipRadius')" - :fallback="previewTheme.radii.tooltip" + :fallback="previewTheme.radii?.tooltip" max="50" hard-min="0" /> @@ -890,7 +905,7 @@ v-model="chatMessageRadiusLocal" name="chatMessageRadius" :label="$t('settings.chatMessageRadius')" - :fallback="previewTheme.radii.chatMessage || 2" + :fallback="previewTheme.radii?.chatMessage || 2" max="50" hard-min="0" /> @@ -996,26 +1011,26 @@ v-model="fontsLocal.interface" name="ui" :label="$t('settings.style.fonts.components.interface')" - :fallback="previewTheme.fonts.interface" + :fallback="previewTheme.fonts?.interface" no-inherit="1" /> <FontControl v-model="fontsLocal.input" name="input" :label="$t('settings.style.fonts.components.input')" - :fallback="previewTheme.fonts.input" + :fallback="previewTheme.fonts?.input" /> <FontControl v-model="fontsLocal.post" name="post" :label="$t('settings.style.fonts.components.post')" - :fallback="previewTheme.fonts.post" + :fallback="previewTheme.fonts?.post" /> <FontControl v-model="fontsLocal.postCode" name="postCode" :label="$t('settings.style.fonts.components.postCode')" - :fallback="previewTheme.fonts.postCode" + :fallback="previewTheme.fonts?.postCode" /> </div> </tab-switcher> diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js @@ -1,7 +1,7 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' import Select from '../select/select.vue' -import { getCssShadow } from '../../services/style_setter/style_setter.js' +import { getCssShadow } from '../../services/theme_data/theme_data.service.js' import { hex2rgb } from '../../services/color_convert/color_convert.js' import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue @@ -11,14 +11,14 @@ <input v-model="selected.y" :disabled="!present" - class="input-number" + class="input input-number" type="number" > <div class="wrap"> <input v-model="selected.y" :disabled="!present" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -38,14 +38,14 @@ <input v-model="selected.x" :disabled="!present" - class="input-number" + class="input input-number" type="number" > <div class="wrap"> <input v-model="selected.x" :disabled="!present" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -129,7 +129,7 @@ v-model="selected.inset" :disabled="!present" name="inset" - class="input-inset visible-for-screenreader-only" + class="input -checkbox input-inset visible-for-screenreader-only" type="checkbox" > <label @@ -153,7 +153,7 @@ v-model="selected.blur" :disabled="!present" name="blur" - class="input-range" + class="input input-range" type="range" max="20" min="0" @@ -161,7 +161,7 @@ <input v-model="selected.blur" :disabled="!present" - class="input-number" + class="input input-number" type="number" min="0" > @@ -181,7 +181,7 @@ v-model="selected.spread" :disabled="!present" name="spread" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -189,7 +189,7 @@ <input v-model="selected.spread" :disabled="!present" - class="input-number" + class="input input-number" type="number" > </div> @@ -219,8 +219,6 @@ <script src="./shadow_control.js"></script> <style lang="scss"> -@import "../../variables"; - .shadow-control { display: flex; flex-wrap: wrap; @@ -237,8 +235,6 @@ display: flex; flex-wrap: wrap; - $side: 15em; - input[type="number"] { width: 5em; min-width: 2em; @@ -261,7 +257,7 @@ .x-shift-control .wrap, input[type="range"] { margin: 0; - width: $side; + width: 15em; height: 2em; } @@ -271,7 +267,7 @@ .wrap { width: 2em; - height: $side; + height: 15em; } input[type="range"] { @@ -293,16 +289,12 @@ linear-gradient(-45deg, transparent 75%, #666 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0; - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border-radius: var(--roundness); .preview-block { width: 33%; height: 33%; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); } } } diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue @@ -5,7 +5,7 @@ > <div class="panel panel-default"> <div - class="panel-heading timeline-heading" + class="panel-heading" :class="{ 'shout-heading': floating }" @click.stop.prevent="togglePanel" > @@ -18,7 +18,7 @@ /> </div> </div> - <div class="shout-window"> + <div class="panel-body shout-window"> <div v-for="message in messages" :key="message.id" @@ -41,10 +41,10 @@ </div> </div> </div> - <div class="shout-input"> + <div class="panel-body shout-input"> <textarea v-model="currentMessage" - class="shout-input-textarea" + class="shout-input-textarea input" rows="1" @keyup.enter="submit(currentMessage)" /> @@ -75,8 +75,6 @@ <script src="./shout_panel.js"></script> <style lang="scss"> -@import "../../variables"; - .floating-shout { position: fixed; bottom: 0.5em; @@ -97,8 +95,7 @@ cursor: pointer; .icon { - color: $fallback--text; - color: var(--panelText, $fallback--text); + color: var(--text); margin-right: 0.5em; } @@ -128,8 +125,7 @@ img { height: 24px; width: 24px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); margin-right: 0.5em; margin-top: 0.25em; } diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -1,6 +1,6 @@ <template> <div - class="side-drawer-container" + class="side-drawer-container mobile-drawer" :class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }" > <div @@ -35,7 +35,10 @@ v-if="!currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'login' }"> + <router-link + :to="{ name: 'login' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -47,7 +50,10 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="timelinesRoute"> + <router-link + :to="timelinesRoute" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -59,7 +65,10 @@ v-if="currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'lists' }"> + <router-link + :to="{ name: 'lists' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -74,6 +83,7 @@ <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }" style="position: relative;" + class="menu-item" > <FAIcon fixed-width @@ -82,7 +92,7 @@ /> {{ $t("nav.chats") }} <span v-if="unreadChatCount" - class="badge badge-notification" + class="badge -notification" > {{ unreadChatCount }} </span> @@ -91,7 +101,10 @@ </ul> <ul v-if="currentUser"> <li @click="toggleDrawer"> - <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> + <router-link + :to="{ name: 'interactions', params: { username: currentUser.screen_name } }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -103,7 +116,10 @@ v-if="currentUser.locked" @click="toggleDrawer" > - <router-link to="/friend-requests"> + <router-link + to="/friend-requests" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -111,7 +127,7 @@ /> {{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" - class="badge badge-notification" + class="badge -notification" > {{ followRequestCount }} </span> @@ -121,7 +137,10 @@ v-if="shout" @click="toggleDrawer" > - <router-link :to="{ name: 'shout-panel' }"> + <router-link + :to="{ name: 'shout-panel' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -135,7 +154,10 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="{ name: 'search' }"> + <router-link + :to="{ name: 'search' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -147,7 +169,10 @@ v-if="currentUser && suggestionsEnabled" @click="toggleDrawer" > - <router-link :to="{ name: 'who-to-follow' }"> + <router-link + :to="{ name: 'who-to-follow' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -157,7 +182,7 @@ </li> <li @click="toggleDrawer"> <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click="openSettingsModal" > <FAIcon @@ -168,7 +193,10 @@ </button> </li> <li @click="toggleDrawer"> - <router-link :to="{ name: 'about'}"> + <router-link + :to="{ name: 'about'}" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -181,7 +209,7 @@ @click="toggleDrawer" > <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click.stop="openAdminModal" > <FAIcon @@ -197,6 +225,7 @@ > <router-link :to="{ name: 'announcements' }" + class="menu-item" > <FAIcon fixed-width @@ -205,7 +234,7 @@ /> {{ $t("nav.announcements") }} <span v-if="unreadAnnouncementCount" - class="badge badge-notification" + class="badge -notification" > {{ unreadAnnouncementCount }} </span> @@ -215,7 +244,10 @@ v-if="currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'edit-navigation' }"> + <router-link + :to="{ name: 'edit-navigation' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -228,7 +260,7 @@ @click="toggleDrawer" > <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click="doLogout" > <FAIcon @@ -251,8 +283,6 @@ <script src="./side_drawer.js"></script> <style lang="scss"> -@import "../../variables"; - .side-drawer-container { position: fixed; z-index: var(--ZI_navbar); @@ -305,17 +335,8 @@ width: 80%; max-width: 20em; flex: 0 0 80%; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--link; - color: var(--popoverText, $fallback--link); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --icon: var(--popoverIcon, $fallback--icon); + box-shadow: var(--shadow); + background-color: var(--background); .badge { margin-left: 10px; @@ -362,8 +383,7 @@ margin: 0; padding: 0; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } .side-drawer ul:last-child { @@ -380,18 +400,6 @@ height: 3em; line-height: 3em; padding: 0 0.7em; - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuPopoverText, $fallback--text); - - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } } } </style> diff --git a/src/components/status/post.style.js b/src/components/status/post.style.js @@ -0,0 +1,42 @@ +export default { + name: 'Post', + selector: '.Status', + states: { + selected: '.-focused' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar', + 'Attachment', + 'PollGraph' + ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'ButtonUnstyled', + 'RichContent', + 'Avatar' + ], + defaultRules: [ + { + directives: { + background: '--bg' + } + }, + { + state: ['selected'], + directives: { + background: '--inheritedBackground, 10' + } + } + ] +} diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -39,7 +39,8 @@ import { faThumbtack, faChevronUp, faChevronDown, - faAngleDoubleRight + faAngleDoubleRight, + faPlay } from '@fortawesome/free-solid-svg-icons' library.add( @@ -59,7 +60,8 @@ library.add( faThumbtack, faChevronUp, faChevronDown, - faAngleDoubleRight + faAngleDoubleRight, + faPlay ) const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1) @@ -152,6 +154,7 @@ const Status = { 'controlledSetMediaPlaying', 'dive' ], + emits: ['interacted'], data () { return { uncontrolledReplying: false, @@ -229,17 +232,14 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, - rtBotStatus () { - return this.statusoid.user.bot - }, botStatus () { - return this.status.user.bot + return this.status.user.actor_type === 'Service' }, - botIndicator () { - return this.botStatus && !this.hideBotIndication + showActorTypeIndicator () { + return !this.hideBotIndication }, - rtBotIndicator () { - return this.rtBotStatus && !this.hideBotIndication + sensitiveStatus () { + return this.status.nsfw }, mentionsLine () { if (!this.headTailLinks) return [] @@ -268,7 +268,9 @@ const Status = { // Wordfiltered this.muteWordHits.length > 0 || // bot status - (this.muteBotStatuses && this.botStatus && !this.compact) + (this.muteBotStatuses && this.botStatus && !this.compact) || + // sensitive status + (this.muteSensitiveStatuses && this.sensitiveStatus && !this.compact) return !this.unmuted && !this.shouldNotMute && reasonsToMute }, userIsMuted () { @@ -371,9 +373,15 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, + shouldDisplayFavsAndRepeats () { + return !this.hidePostStats && this.isFocused && (this.combinedFavsAndRepeatsUsers.length > 0 || this.statusFromGlobalRepository.quotes_count) + }, muteBotStatuses () { return this.mergedConfig.muteBotStatuses }, + muteSensitiveStatuses () { + return this.mergedConfig.muteSensitiveStatuses + }, hideBotIndication () { return this.mergedConfig.hideBotIndication }, @@ -415,6 +423,32 @@ const Status = { }, shouldDisplayQuote () { return this.quotedStatus && this.displayQuote + }, + scrobblePresent () { + if (this.mergedConfig.hideScrobbles) return false + if (!this.status.user.latestScrobble) return false + const value = this.mergedConfig.hideScrobblesAfter.match(/\d+/gs)[0] + const unit = this.mergedConfig.hideScrobblesAfter.match(/\D+/gs)[0] + let multiplier = 60 * 1000 // minutes is smallest unit + switch (unit) { + case 'm': + break + case 'h': + multiplier *= 60 // hour + break + case 'd': + multiplier *= 60 // hour + multiplier *= 24 // day + break + } + const maxAge = Number(value) * multiplier + const createdAt = Date.parse(this.status.user.latestScrobble.created_at) + const age = Date.now() - createdAt + if (age > maxAge) return false + return this.status.user.latestScrobble.artist + }, + scrobble () { + return this.status.user.latestScrobble } }, methods: { @@ -434,9 +468,11 @@ const Status = { this.error = error }, clearError () { + this.$emit('interacted') this.error = undefined }, toggleReplying () { + this.$emit('interacted') controlledOrUncontrolledToggle(this, 'replying') }, gotoOriginal (id) { diff --git a/src/components/status/status.scss b/src/components/status/status.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Status { min-width: 0; white-space: normal; @@ -12,24 +10,8 @@ --_still-image-label-visibility: hidden; } - &.-focused { - background-color: $fallback--lightBg; - background-color: var(--selectedPost, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedPostText, $fallback--text); - - --lightText: var(--selectedPostLightText, $fallback--light); - --faint: var(--selectedPostFaintText, $fallback--faint); - --faintLink: var(--selectedPostFaintLink, $fallback--faint); - --postLink: var(--selectedPostPostLink, $fallback--faint); - --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint); - --icon: var(--selectedPostIcon, $fallback--icon); - } - .gravestone { - padding: var(--status-margin, $status-margin); - color: $fallback--faint; - color: var(--faint, $fallback--faint); + padding: var(--status-margin); display: flex; .deleted-text { @@ -40,7 +22,7 @@ .status-container { display: flex; - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); > * { min-width: 0; @@ -52,7 +34,7 @@ } .pin { - padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0; + padding: var(--status-margin) var(--status-margin) 0; display: flex; align-items: center; justify-content: flex-end; @@ -68,7 +50,7 @@ } .left-side { - margin-right: var(--status-margin, $status-margin); + margin-right: var(--status-margin); } .right-side { @@ -77,7 +59,7 @@ } .usercard { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .status-username { @@ -135,11 +117,6 @@ .button-unstyled { padding: 5px; margin: -5px; - - &:hover svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } .svg-inline--fa { @@ -243,16 +220,15 @@ } .repeat-info { - padding: 0.4em var(--status-margin, $status-margin); + padding: 0.4em var(--status-margin); .repeat-icon { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } } .repeater-avatar { - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); margin-left: 28px; width: 20px; height: 20px; @@ -289,7 +265,7 @@ position: relative; width: 100%; display: flex; - margin-top: var(--status-margin, $status-margin); + margin-top: var(--status-margin); > * { max-width: 4em; @@ -357,7 +333,7 @@ } .favs-repeated-users { - margin-top: var(--status-margin, $status-margin); + margin-top: var(--status-margin); } .stats { @@ -368,10 +344,10 @@ .avatar-row { flex: 1; - overflow: hidden; position: relative; display: flex; align-items: center; + overflow: hidden; &::before { content: ""; @@ -379,16 +355,16 @@ height: 100%; width: 1px; left: 0; - background-color: var(--faint, $fallback--faint); + background-color: var(--textFaint); } } .stat-count { - margin-right: var(--status-margin, $status-margin); + margin-right: var(--status-margin); user-select: none; .stat-title { - color: var(--faint, $fallback--faint); + color: var(--textFaint); font-size: 0.85em; text-transform: uppercase; position: relative; @@ -398,6 +374,7 @@ font-weight: bolder; font-size: 1.1em; line-height: 1em; + color: var(--text); } &:hover .stat-title { @@ -425,8 +402,8 @@ .quoted-status { margin-top: 0.5em; - border: 1px solid var(--border, $fallback--border); - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); &.-unavailable-prompt { padding: 0.5em; diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -31,6 +31,12 @@ /> </small> <small + v-if="muteSensitiveStatuses && status.nsfw" + class="mute-thread" + > + {{ $t('status.sensitive_muted') }} + </small> + <small v-if="showReasonMutedThread" class="mute-thread" > @@ -79,7 +85,7 @@ <UserAvatar v-if="retweet" class="left-side repeater-avatar" - :bot="rtBotIndicator" + :show-actor-type-indicator="showActorTypeIndicator" :better-shadow="betterShadow" :user="statusoid.user" /> @@ -133,7 +139,7 @@ > <UserAvatar class="post-avatar" - :bot="botIndicator" + :show-actor-type-indicator="showActorTypeIndicator" :compact="compact" :better-shadow="betterShadow" :user="status.user" @@ -180,7 +186,7 @@ <span class="heading-right"> <router-link - class="timeago faint-link" + class="timeago faint" :to="{ name: 'conversation', params: { id: status.id } }" > <Timeago @@ -250,6 +256,47 @@ </span> </div> <div + v-if="scrobblePresent" + class="status-rich-presence" + > + <a + v-if="scrobble.externalLink" + :href="scrobble.externalLink" + target="_blank" + > + {{ scrobble.artist }} — {{ scrobble.title }} + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="play" + /> + <span class="status-rich-presence-time"> + <Timeago + template-key="time.in_past" + :time="scrobble.created_at" + :auto-update="60" + /> + </span> + </a> + <span v-if="!scrobble.externalLink"> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="music" + /> + {{ scrobble.artist }} — {{ scrobble.title }} + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="play" + /> + <span class="status-rich-presence-time"> + <Timeago + template-key="time.in_past" + :time="scrobble.created_at" + :auto-update="60" + /> + </span> + </span> + </div> + <div v-if="isReply || hasMentionsLine" class="heading-reply-row" > @@ -409,7 +456,7 @@ > <button v-if="showOtherRepliesAsButton && replies.length > 1" - class="button-unstyled -link faint" + class="button-unstyled -link" :title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })" @click.prevent="dive" > @@ -437,7 +484,7 @@ <transition name="fade"> <div - v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0" + v-if="shouldDisplayFavsAndRepeats" class="favs-repeated-users" > <div class="stats"> @@ -465,6 +512,19 @@ </div> </div> </UserListPopover> + <router-link + v-if="statusFromGlobalRepository.quotes_count > 0" + :to="{ name: 'quotes', params: { id: status.id } }" + > + <div + class="stat-count" + > + <a class="stat-title">{{ $t('status.quotes') }}</a> + <div class="stat-number"> + {{ statusFromGlobalRepository.quotes_count }} + </div> + </div> + </router-link> <div class="avatar-row"> <AvatarList :users="combinedFavsAndRepeatsUsers" /> </div> @@ -490,14 +550,17 @@ :visibility="status.visibility" :logged-in="loggedIn" :status="status" + @click="$emit('interacted')" /> <favorite-button :logged-in="loggedIn" :status="status" + @click="$emit('interacted')" /> <ReactButton v-if="loggedIn" :status="status" + @click="$emit('interacted')" /> <extra-buttons :status="status" @@ -515,7 +578,7 @@ <UserAvatar class="post-avatar" :compact="compact" - :bot="botIndicator" + :show-actor-type-indicator="showActorTypeIndicator" /> </div> <div class="right-side"> diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .StatusBody { display: flex; flex-direction: column; @@ -14,7 +12,6 @@ & .text, & .summary { - font-family: var(--postFont, sans-serif); white-space: pre-wrap; overflow-wrap: break-word; word-wrap: break-word; @@ -41,7 +38,7 @@ margin-bottom: 0.5em; border-style: solid; border-width: 0 0 1px; - border-color: var(--border, $fallback--border); + border-color: var(--border); flex-grow: 0; &.-tall { @@ -112,15 +109,6 @@ } } - .greentext { - color: $fallback--cGreen; - color: var(--postGreentext, $fallback--cGreen); - } - - .cyantext { - color: var(--postCyantext, $fallback--cBlue); - } - &.-compact { align-items: top; flex-direction: row; diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue @@ -11,6 +11,7 @@ > <RichContent class="media-body summary" + :faint="compact" :html="status.summary_raw_html" :emoji="status.emojis" /> @@ -48,6 +49,7 @@ :html="status.raw_html" :emoji="status.emojis" :handle-links="true" + :faint="compact" :greentext="mergedConfig.greentext" :attentions="status.attentions" @parseReady="onParseReady" diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue @@ -40,19 +40,14 @@ <script src="./status_popover.js"></script> <style lang="scss"> -@import "../../variables"; - /* popover styles load on-demand, so we need to override */ .status-popover.popover { font-size: 1rem; min-width: 15em; max-width: 95%; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); border-style: solid; border-width: 1px; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); /* TODO cleanup this */ .Status.Status { diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue @@ -32,8 +32,6 @@ <script src="./sticker_picker.js"></script> <style lang="scss"> -@import "../../variables"; - .sticker-picker { width: 100%; @@ -56,7 +54,7 @@ height: 100%; &:hover { - filter: drop-shadow(0 0 5px var(--accent, $fallback--link)); + filter: drop-shadow(0 0 5px var(--accent)); } } } diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue @@ -28,8 +28,6 @@ <script src="./still-image.js"></script> <style lang="scss"> -@import "../../variables"; - .still-image { position: relative; line-height: 0; @@ -68,8 +66,7 @@ color: #fff; display: block; padding: 2px 4px; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); z-index: 2; visibility: var(--_still-image-label-visibility, visible); } diff --git a/src/components/tab_switcher/tab.style.js b/src/components/tab_switcher/tab.style.js @@ -0,0 +1,78 @@ +export default { + name: 'Tab', // Name of the component + selector: '.tab', // CSS selector/prefix + states: { + active: '.active', + hover: ':hover:not(.disabled)', + disabled: '.disabled' + }, + validInnerComponents: [ + 'Text', + 'Icon' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] + } + }, + { + state: ['active'], + directives: { + opacity: 0 + } + }, + { + state: ['hover', 'active'], + directives: { + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: ['--defaultButtonBevel'] + } + }, + { + component: 'Text', + parent: { + component: 'Tab', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'Tab', + state: ['active'] + }, + directives: { + textColor: '--text' + } + }, + { + component: 'Icon', + parent: { + component: 'Tab', + state: ['active', 'hover'] + }, + directives: { + textColor: '--text' + } + } + ] +} diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx @@ -97,7 +97,7 @@ export default { .map((slot, index) => { const props = slot.props if (!props) return - const classesTab = ['tab', 'button-default'] + const classesTab = ['tab'] const classesWrapper = ['tab-wrapper'] if (this.activeIndex === index) { classesTab.push('active') diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - /* stylelint-disable no-descending-specificity */ .tab-switcher { display: flex; @@ -25,8 +23,7 @@ content: ""; flex: 1 1 auto; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } .tab-wrapper { @@ -37,8 +34,7 @@ right: 0; bottom: 0; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } } @@ -80,8 +76,7 @@ flex-basis: 0.5em; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::after { @@ -106,16 +101,14 @@ right: 0; bottom: 0; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::before { flex: 0 0 6px; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &:last-child .tab { @@ -173,6 +166,15 @@ } .tab { + user-select: none; + color: var(--text); + border: none; + cursor: pointer; + box-shadow: var(--shadow); + font-size: 1em; + font-family: var(--font); + border-radius: var(--roundness); + background-color: var(--background); position: relative; white-space: nowrap; padding: 6px 1em; @@ -188,8 +190,6 @@ &.active { background: transparent; z-index: 5; - color: $fallback--text; - color: var(--tabActiveText, $fallback--text); } img { @@ -231,7 +231,7 @@ margin-top: 0.5em; margin-left: 0.2em; margin-bottom: 0.25em; - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); @media all and (min-width: 800px) { display: none; diff --git a/src/components/text.style.js b/src/components/text.style.js @@ -0,0 +1,22 @@ +export default { + name: 'Text', + selector: '/*text*/', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + directives: { + textColor: '--text', + textAuto: 'no-preserve' + } + }, + { + state: ['faint'], + directives: { + textOpacity: 0.5 + } + } + ] +} diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue @@ -119,15 +119,13 @@ <script src="./thread_tree.js"></script> <style lang="scss"> -@import "../../variables"; - .thread-tree-replies { - margin-left: var(--status-margin, $status-margin); - border-left: 2px solid var(--border, $fallback--border); + margin-left: var(--status-margin); + border-left: 2px solid var(--border); } .thread-tree-replies-hidden { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); /* Make the button stretch along the whole row */ display: flex; diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js @@ -25,6 +25,7 @@ const Timeline = { 'title', 'userId', 'listId', + 'statusId', 'tag', 'embedded', 'count', @@ -77,13 +78,13 @@ const Timeline = { } }, classes () { - let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-nonpanel'] + let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-embedded'] if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention']) return { root: rootClasses, - header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : []), - body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []), - footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : []) + header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : ['panel-body']), + body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : ['panel-body']), + footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : ['panel-body']) } }, // id map of statuses which need to be hidden in the main list due to pinning logic @@ -121,6 +122,7 @@ const Timeline = { showImmediately, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }) }, @@ -183,6 +185,7 @@ const Timeline = { showImmediately: true, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }).then(({ statuses }) => { if (statuses && statuses.length === 0) { diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss @@ -1,31 +1,20 @@ -@import "../../variables"; - .Timeline { - .alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: var(--badgeNeutral); + .timeline-body { + background: none; + backdrop-filter: none; } .alert-badge { font-size: 0.75em; line-height: 1; text-align: right; - border-radius: var(--tooltipRadius); + border-radius: var(--roundness); position: absolute; left: calc(50% - 0.5em); top: calc(50% - 0.4em); padding: 0.2em; margin-left: 0.7em; margin-top: -1em; - background-color: var(--badgeNeutral); - color: var(--badgeNeutralText); } .loadmore-button { @@ -41,12 +30,17 @@ z-index: 2; } - &.-nonpanel { + &.-embedded { .timeline-heading { text-align: center; line-height: 2.75em; padding: 0 0.5em; + // Override the shrug empty filler + &:empty::before { + content: initial; + } + .button-default, .alert { line-height: 2em; diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -38,7 +38,7 @@ fixed-width icon="circle-plus" /> - <div class="alert-badge"> + <div class="badge -counter"> {{ mobileLoadButtonString }} </div> </button> diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js @@ -19,7 +19,8 @@ export const timelineNames = () => { bookmarks: 'nav.bookmarks', dms: 'nav.dms', 'public-timeline': 'nav.public_tl', - 'public-external-timeline': 'nav.twkn' + 'public-external-timeline': 'nav.twkn', + quotes: 'nav.quotes' } } diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue @@ -45,8 +45,6 @@ <script src="./timeline_menu.js"></script> <style lang="scss"> -@import "../../variables"; - .timeline-menu-popover { min-width: 24rem; max-width: 100vw; @@ -60,65 +58,6 @@ margin: 0; padding: 0; } - - a { - display: block; - padding: 0 0.65em; - height: 3.5em; - line-height: 3.5em; - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - - &.router-link-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - - &:hover { - text-decoration: underline; - } - } - - svg { - margin-right: 0.4em; - margin-left: -0.2em; - } - } - - li { - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - padding: 0; - - &:last-child a { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); - } - - &:last-child { - border: none; - } - } } .TimelineMenu { @@ -159,8 +98,6 @@ } &.open .timeline-menu-title svg { - color: $fallback--text; - color: var(--panelText, $fallback--text); transform: rotate(180deg); } diff --git a/src/components/top_bar.style.js b/src/components/top_bar.style.js @@ -0,0 +1,28 @@ +export default { + name: 'TopBar', + selector: 'nav', + validInnerComponents: [ + 'Link', + 'Text', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Input', + 'Badge' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js @@ -0,0 +1,19 @@ +export default { + name: 'Underlay', + selector: '#content', + // Out of tree selector: Most components are laid over underlay, but underlay itself is not part of the DOM tree, + // i.e. it's a separate absolutely-positioned component, so we need to treat it differently depending on whether + // we are searching for underlay specifically or for whatever is laid on top of it. + outOfTreeSelector: '.underlay', + validInnerComponents: [ + 'Panel' + ], + defaultRules: [ + { + directives: { + background: '#000000', + opacity: 0.2 + } + } + ] +} diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss @@ -1,5 +1,3 @@ -@import "src/variables"; - .UpdateNotification { overflow: hidden; } @@ -48,7 +46,7 @@ .panel-body { border-width: 0 0 1px; border-style: solid; - border-color: var(--border, $fallback--border); + border-color: var(--border); } .panel-footer { diff --git a/src/components/user_avatar/avatar.style.js b/src/components/user_avatar/avatar.style.js @@ -0,0 +1,22 @@ +export default { + name: 'Avatar', + selector: '.Avatar', + variants: { + compact: '.-compact' + }, + defaultRules: [ + { + directives: { + roundness: 3, + shadow: [{ + x: 0, + y: 1, + blur: 8, + spread: 0, + color: '#000000', + alpha: 0.7 + }] + } + } + ] +} diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js @@ -3,11 +3,13 @@ import StillImage from '../still-image/still-image.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faRobot + faRobot, + faPeopleGroup } from '@fortawesome/free-solid-svg-icons' library.add( - faRobot + faRobot, + faPeopleGroup ) const UserAvatar = { @@ -15,7 +17,7 @@ const UserAvatar = { 'user', 'betterShadow', 'compact', - 'bot' + 'showActorTypeIndicator' ], data () { return { diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue @@ -18,21 +18,24 @@ :class="{ '-compact': compact }" /> <FAIcon - v-if="bot" + v-if="showActorTypeIndicator && user?.actor_type === 'Service'" icon="robot" - class="bot-indicator" + class="actor-type-indicator" + /> + <FAIcon + v-if="showActorTypeIndicator && user?.actor_type === 'Group'" + icon="people-group" + class="actor-type-indicator" /> </span> </template> <script src="./user_avatar.js"></script> <style lang="scss"> -@import "../../variables"; - .Avatar { - --_avatarShadowBox: var(--avatarStatusShadow); - --_avatarShadowFilter: var(--avatarStatusShadowFilter); - --_avatarShadowInset: var(--avatarStatusShadowInset); + --_avatarShadowBox: var(--shadow); + --_avatarShadowFilter: var(--shadowFilter); + --_avatarShadowInset: var(--shadowInset); --_still-image-label-visibility: hidden; display: inline-block; @@ -43,16 +46,14 @@ &.-compact { width: 32px; height: 32px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); } .avatar { width: 100%; height: 100%; box-shadow: var(--_avatarShadowBox); - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); &.-better-shadow { box-shadow: var(--_avatarShadowInset); @@ -64,13 +65,11 @@ } &.-compact { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); } &.-placeholder { - background-color: $fallback--fg; - background-color: var(--fg, $fallback--fg); + background-color: var(--background); } } @@ -79,7 +78,7 @@ height: 100%; } - .bot-indicator { + .actor-type-indicator { position: absolute; bottom: 0; right: 0; @@ -87,7 +86,7 @@ padding: 0.2em; background: rgb(127 127 127 / 50%); color: #fff; - border-radius: var(--tooltipRadius); + border-radius: var(--roundness); } } </style> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -225,7 +225,7 @@ export default { this.$store.dispatch('setCurrentMedia', attachment) }, mentionUser () { - this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) + this.$store.dispatch('openPostStatusModal', { profileMention: true, repliedUser: this.user }) }, onAvatarClickHandler (e) { if (this.onAvatarClick) { diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .user-card { position: relative; z-index: 1; @@ -21,14 +19,6 @@ position: relative; } - .panel-body { - word-wrap: break-word; - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; - // create new stacking context - position: relative; - } - .background-image { position: absolute; top: 0; @@ -62,11 +52,6 @@ padding: 1em; margin: 0; - a { - color: $fallback--link; - color: var(--postLink, $fallback--link); - } - img { object-fit: contain; vertical-align: middle; @@ -76,53 +61,37 @@ } &.-rounded-t { - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: var(--roundness); + border-top-right-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); + --__roundnessTop: var(--roundness); --__roundnessBottom: 0; } &.-rounded { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); - --__roundnessBottom: var(--panelRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-popover { - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--tooltipRadius); - --__roundnessBottom: var(--tooltipRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-bordered { border-width: 1px; border-style: solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } } .user-info { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); padding: 0 26px; - a { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - - &:hover { - color: var(--icon); - } - } - .container { min-width: 0; padding: 16px 0 6px; @@ -164,8 +133,7 @@ display: flex; justify-content: center; align-items: center; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); opacity: 0; transition: opacity 0.2s ease; @@ -188,8 +156,7 @@ padding: 0.5em 0; &:not(:hover) .icon { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--lightText); } } @@ -203,6 +170,7 @@ } .user-screen-name { + color: var(--text); min-width: 1px; flex: 0 1 auto; text-overflow: ellipsis; @@ -214,16 +182,11 @@ flex: 0 0 auto; margin-left: 1em; font-size: 0.7em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } .user-role { flex: none; - color: $fallback--text; - color: var(--alertNeutralText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--alertNeutral, $fallback--fg); } } @@ -241,6 +204,11 @@ --emoji-size: 1.7em; + .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + --link: var(--text) !important; + } + .top-line, .bottom-line { display: flex; @@ -334,8 +302,6 @@ padding: 0.5em 1.5em 0; text-align: center; justify-content: space-between; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); flex-wrap: wrap; } diff --git a/src/components/user_card/user_card.style.js b/src/components/user_card/user_card.style.js @@ -0,0 +1,41 @@ +export default { + name: 'UserCard', + selector: '.user-card', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Input', + 'RichContent', + 'Alert' + ], + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0, + roundness: 3, + shadow: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }], + '--profileTint': 'color | $alpha(--background, 0.5)' + } + }, + { + parent: { + component: 'UserCard' + }, + component: 'RichContent', + directives: { + opacity: 0 + } + } + ] +} diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -113,22 +113,28 @@ <template v-if="!hideBio"> <span v-if="user.deactivated" - class="alert user-role" + class="alert neutral user-role" > {{ $t('user_card.deactivated') }} </span> <span v-if="!!visibleRole" - class="alert user-role" + class="alert neutral user-role" > {{ $t(`general.role.${visibleRole}`) }} </span> <span - v-if="user.bot" - class="alert user-role" + v-if="user.actor_type === 'Service'" + class="alert neutral user-role" > {{ $t('user_card.bot') }} </span> + <span + v-if="user.actor_type === 'Group'" + class="alert user-role" + > + {{ $t('user_card.group') }} + </span> </template> <span v-if="user.locked"> <FAIcon @@ -160,14 +166,14 @@ v-if="userHighlightType !== 'disabled'" :id="'userHighlightColorTx'+user.id" v-model="userHighlightColor" - class="userHighlightText" + class="input userHighlightText" type="text" > <input v-if="userHighlightType !== 'disabled'" :id="'userHighlightColor'+user.id" v-model="userHighlightColor" - class="userHighlightCl" + class="input userHighlightCl" type="color" > {{ ' ' }} @@ -276,10 +282,7 @@ /> </div> </div> - <div - v-if="!hideBio" - class="panel-body" - > + <div v-if="!hideBio"> <div v-if="!mergedConfig.hideUserStats && switcher" class="user-counts" diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue @@ -10,11 +10,11 @@ <button v-for="list in lists" :key="list.id" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleList(list.id)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': list.inList }" /> {{ list.title }} @@ -22,7 +22,7 @@ </div> </template> <template #trigger> - <button class="btn button-default dropdown-item -has-submenu"> + <button class="menu-item dropdown-item -has-submenu"> {{ $t('lists.manage_lists') }} <FAIcon class="chevron-icon" diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue @@ -48,8 +48,6 @@ <script src="./user_list_popover.js"></script> <style lang="scss"> -@import "../../variables"; - .user-list-popover { padding: 0.5em; diff --git a/src/components/user_note/user_note.vue b/src/components/user_note/user_note.vue @@ -33,7 +33,7 @@ <textarea v-show="editing" v-model="localNote" - class="note-text" + class="input note-text" /> <span v-show="!editing" @@ -48,8 +48,6 @@ <script src="./user_note.js"></script> <style lang="scss"> -@import "../../variables"; - .user-note { display: flex; flex-direction: column; @@ -82,7 +80,7 @@ .note-text.-blank { font-style: italic; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } } </style> diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue @@ -22,8 +22,15 @@ <script src="./user_panel.js"></script> <style lang="scss"> -.user-panel .signed-in { - overflow: visible; - z-index: 10; +.user-panel { + .panel { + background: var(--background); + backdrop-filter: var(--backdrop-filter); + } + + .signed-in { + overflow: visible; + z-index: 10; + } } </style> diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue @@ -24,8 +24,6 @@ <script src="./user_popover.js"></script> <style lang="scss"> -@import "../../variables"; - /* popover styles load on-demand, so we need to override */ /* stylelint-disable block-no-empty */ .user-popover.popover { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js @@ -80,6 +80,9 @@ const UserProfile = { followersTabVisible () { return this.isUs || !this.user.hide_followers }, + favoritesTabVisible () { + return this.isUs || !this.user.hide_favorites + }, formattedBirthday () { const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' }) @@ -103,6 +106,8 @@ const UserProfile = { startFetchingTimeline('user', userId) startFetchingTimeline('media', userId) if (this.isUs) { + startFetchingTimeline('favorites') + } else if (!this.user.hide_favorites) { startFetchingTimeline('favorites', userId) } // Fetch all pinned statuses immediately diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -4,52 +4,54 @@ v-if="user" class="user-profile panel panel-default" > - <UserCard - :user-id="userId" - :switcher="true" - :selected="timeline.viewing" - avatar-action="zoom" - rounded="top" - :has-note-editor="true" - /> - <span - v-if="!!user.birthday" - class="user-birthday" - > - <FAIcon - class="fa-old-padding" - icon="birthday-cake" + <div class="panel-body"> + <UserCard + :user-id="userId" + :switcher="true" + :selected="timeline.viewing" + avatar-action="zoom" + rounded="top" + :has-note-editor="true" /> - {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} - </span> - <div - v-if="user.fields_html && user.fields_html.length > 0" - class="user-profile-fields" - > - <dl - v-for="(field, index) in user.fields_html" - :key="index" - class="user-profile-field" + <span + v-if="!!user.birthday" + class="user-birthday" > - <dt - :title="user.fields_text[index].name" - class="user-profile-field-name" - > - <RichContent - :html="field.name" - :emoji="user.emoji" - /> - </dt> - <dd - :title="user.fields_text[index].value" - class="user-profile-field-value" + <FAIcon + class="fa-old-padding" + icon="birthday-cake" + /> + {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} + </span> + <div + v-if="user.fields_html && user.fields_html.length > 0" + class="user-profile-fields" + > + <dl + v-for="(field, index) in user.fields_html" + :key="index" + class="user-profile-field" > - <RichContent - :html="field.value" - :emoji="user.emoji" - /> - </dd> - </dl> + <dt + :title="user.fields_text[index].name" + class="user-profile-field-name" + > + <RichContent + :html="field.name" + :emoji="user.emoji" + /> + </dt> + <dd + :title="user.fields_text[index].value" + class="user-profile-field-value" + > + <RichContent + :html="field.value" + :emoji="user.emoji" + /> + </dd> + </dl> + </div> </div> <tab-switcher :active-tab="tab" @@ -72,10 +74,14 @@ <div v-if="followsTabVisible" key="followees" + class="panel-body" :label="$t('user_card.followees')" :disabled="!user.friends_count" > - <FriendList :user-id="userId"> + <FriendList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" /> </template> @@ -84,10 +90,14 @@ <div v-if="followersTabVisible" key="followers" + class="panel-body" :label="$t('user_card.followers')" :disabled="!user.followers_count" > - <FollowerList :user-id="userId"> + <FollowerList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" @@ -109,7 +119,7 @@ :footer-slipgate="footerRef" /> <Timeline - v-if="isUs" + v-if="favoritesTabVisible" key="favorites" :label="$t('user_card.favorites')" :disabled="!favorites.visibleStatuses.length" @@ -117,6 +127,7 @@ :title="$t('user_card.favorites')" timeline-name="favorites" :timeline="favorites" + :user-id="isUs ? undefined : userId" :in-profile="true" :footer-slipgate="footerRef" /> @@ -135,7 +146,7 @@ {{ $t('settings.profile_tab') }} </div> </div> - <div class="panel-body"> + <div> <span v-if="error">{{ error }}</span> <FAIcon v-else @@ -150,8 +161,6 @@ <script src="./user_profile.js"></script> <style lang="scss"> -@import "../../variables"; - .user-profile { flex: 2; flex-basis: 500px; @@ -181,9 +190,8 @@ .user-profile-field { display: flex; margin: 0.25em; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); .user-profile-field-name { flex: 0 1 30%; @@ -191,7 +199,7 @@ text-align: right; color: var(--lightText); min-width: 120px; - border-right: 1px solid var(--border, $fallback--border); + border-right: 1px solid var(--border); } .user-profile-field-value { @@ -228,4 +236,5 @@ padding: 7em; } } + </style> diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -19,7 +19,7 @@ <p>{{ $t('user_reporting.add_comment_description') }}</p> <textarea v-model="comment" - class="form-control" + class="input form-control" :placeholder="$t('user_reporting.additional_comments')" rows="1" @input="resize" @@ -72,8 +72,6 @@ <script src="./user_reporting_modal.js"></script> <style lang="scss"> -@import "../../variables"; - .user-reporting-panel { width: 90vw; max-width: 700px; @@ -84,8 +82,7 @@ display: flex; flex-direction: column-reverse; border-top: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); overflow: hidden; } @@ -155,8 +152,7 @@ width: 50%; max-width: 320px; border-right: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); padding: 1.1em; > div { diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue @@ -2,7 +2,7 @@ <video class="video" preload="metadata" - :src="attachment.url" + :src="attachment.url + '#t=0.00000000000001'" :loop="loopVideo" :controls="controls" :alt="attachment.description" diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss @@ -1,12 +1,9 @@ -@import "../../variables"; - .with-load-more { &-footer { padding: 10px; text-align: center; border-top: 1px solid; - border-top-color: $fallback--border; - border-top-color: var(--border, $fallback--border); + border-top-color: var(--border); .error { font-size: 1rem; diff --git a/src/i18n/cs.json b/src/i18n/cs.json @@ -9,7 +9,10 @@ "scope_options": "Možnosti rozsahů", "text_limit": "Textový limit", "title": "Vlastnosti", - "who_to_follow": "Koho sledovat" + "who_to_follow": "Koho sledovat", + "shout": "Shoutbox", + "pleroma_chat_messages": "Pleroma Chat", + "upload_limit": "Limit pro velikost souborů" }, "finder": { "error_fetching_user": "Chyba při načítání uživatele", @@ -20,12 +23,47 @@ "submit": "Odeslat", "more": "Více", "generic_error": "Vyskytla se chyba", - "optional": "volitelné" + "optional": "volitelné", + "retry": "Zkuste to znovu", + "show_more": "Zobrazit více", + "show_less": "Zobrazit méně", + "never_show_again": "Znovu již nezobrazovat", + "dismiss": "Zahodit", + "cancel": "Zrušit", + "disable": "Vypnout", + "enable": "Zapnout", + "close": "Zavřít", + "peek": "Nahlédnout", + "generic_error_message": "Došlo k chybě: {0}", + "error_retry": "Zkuste to prosím znovu", + "confirm": "Potvrdit", + "verify": "Ověřit", + "scope_in_timeline": { + "public": "Veřejné", + "direct": "Přímá", + "unlisted": "Neuvedené", + "private": "Pouze pro sledující" + }, + "scroll_to_top": "Přejít na začátek", + "role": { + "admin": "Správce", + "moderator": "Moderátor" + }, + "pin": "Připnout položku", + "flash_content": "Klikněte pro zobrazení Flash obsahu pomocí Ruffle (Experimentální, nemusí fungovat).", + "flash_security": "Flash obsah může být nebezpečný, protože se jedná o libovolný spustitelný kód.", + "flash_fail": "Nepodařilo se načíst Flash obsah. Podrobnosti naleznete v konzoli.", + "undo": "Vrátit zpět", + "yes": "Ano", + "no": "Ne", + "unpin": "Odepnout položku", + "loading": "Načítání…" }, "image_cropper": { "crop_picture": "Oříznout obrázek", "save": "Uložit", - "cancel": "Zrušit" + "cancel": "Zrušit", + "save_without_cropping": "Uložit bez ořezávání" }, "login": { "login": "Přihlásit", @@ -35,17 +73,31 @@ "placeholder": "např. lain", "register": "Registrovat", "username": "Uživatelské jméno", - "hint": "Chcete-li se přidat do diskuze, přihlaste se" + "hint": "Chcete-li se přidat do diskuze, přihlaste se", + "logout_confirm": "Opravdu se chcete odhlásit?", + "logout_confirm_accept_button": "Odhlásit se", + "logout_confirm_cancel_button": "Neodhlašovat", + "logout_confirm_title": "Potvrzení odhlášení", + "authentication_code": "Ověřovací kód", + "enter_recovery_code": "Zadejte záložní kód", + "enter_two_factor_code": "Zadejte dvoufaktorový ověřovací kód", + "recovery_code": "Záložní kód", + "heading": { + "totp": "Dvoufaktorové ověřování", + "recovery": "Dvoufaktorové obnovení" + } }, "media_modal": { "previous": "Předchozí", - "next": "Další" + "next": "Další", + "counter": "{current} / {total}", + "hide": "Zavřít prohlížeč médií" }, "nav": { "about": "O instanci", "back": "Zpět", "chat": "Místní chat", - "friend_requests": "Požadavky o sledování", + "friend_requests": "Požadavky na sledování", "mentions": "Zmínky", "dms": "Přímé zprávy", "public_tl": "Veřejná časová osa", @@ -53,7 +105,24 @@ "twkn": "Celá známá síť", "user_search": "Hledání uživatelů", "who_to_follow": "Koho sledovat", - "preferences": "Předvolby" + "preferences": "Předvolby", + "home_timeline": "Domovská časová osa", + "timelines": "Časové osy", + "search_close": "Zavřít vyhledávací panel", + "chats": "Chaty", + "lists": "Seznamy", + "edit_nav_mobile": "Upravit navigační panel", + "mobile_sidebar": "Přepnout mobilní postranní panel", + "announcements": "Oznámení", + "mobile_notifications_close": "Uzavřít oznámení", + "mobile_notifications": "Otevřít oznámení (máte nějaké nepřečtené)", + "administration": "Správa", + "bookmarks": "Záložky", + "search": "Hledat", + "edit_pinned": "Upravit připnuté položky", + "edit_finish": "Dokončit úpravu", + "mobile_notifications_mark_as_seen": "Označit vše jako přečtené", + "interactions": "Interakce" }, "notifications": { "broken_favorite": "Neznámý příspěvek, hledám jej…", @@ -61,9 +130,21 @@ "followed_you": "vás nyní sleduje", "load_older": "Načíst starší oznámení", "notifications": "Oznámení", - "read": "Číst!", + "read": "Přečíst!", "repeated_you": "zopakoval/a váš příspěvek", - "no_more_notifications": "Žádná další oznámení" + "no_more_notifications": "Žádná další oznámení", + "error": "Nastala chyba při načítání oznámení: {0}", + "unread_announcements": "{num} nepřečtené oznámení | {num} nepřečtených oznámení", + "unread_chats": "{num} nepřečtených zpráv | {num} nepřečtených zpráv", + "unread_follow_requests": "{num} nový požadavek o sledování | {num} nových požadavků o sledování", + "configuration_tip": "Může upravit co zde zobrazovat v {theSettings}. {dismiss}", + "follow_request": "vás chce sledovat", + "migrated_to": "migroval na", + "poll_ended": "anketa skončila", + "reacted_with": "reagoval/a s {0}", + "submitted_report": "Odeslal/a stížnost", + "configuration_tip_settings": "nastavení", + "configuration_tip_dismiss": "Již nezobrazovat" }, "post_status": { "new_status": "Napsat nový příspěvek", @@ -85,7 +166,27 @@ "private": "Pouze pro sledující - Poslat pouze sledujícím", "public": "Veřejný - Poslat na veřejné časové osy", "unlisted": "Neuvedený - Neposlat na veřejné časové osy" - } + }, + "edit_unsupported_warning": "Pleroma nepodporuje upravování zmínek a anket.", + "edit_status": "Upravit příspěvek", + "media_description": "Popis médií", + "reply_option": "Odpovědět na tento příspěvek", + "content_type_selection": "Formát příspěvku", + "post": "Odeslat", + "empty_status_error": "Nemůžete odeslat prázdný příspěvek bez žádných souborů", + "preview_empty": "Prázdné", + "media_description_error": "Selhání při aktualizaci médií, zkuste to znovu", + "scope_notice": { + "public": "Tento příspěvek bude viditelný pro všechny", + "private": "Tento příspěvek bude viditelný pouze pro vaše sledující", + "unlisted": "Tento příspěvek nebude viditelný ve Veřejné časové ose a časové ose Celá známá síť" + }, + "scope_notice_dismiss": "Zavřít tuto zprávu", + "quote_option": "Citovat tento příspěvek", + "direct_warning_to_all": "Tento příspěvek budou vidět pouze zmínění uživatelé.", + "direct_warning_to_first_only": "Tento příspěvek bude viditelný pouze pro zmíněné uživatele na začátku příspěvku.", + "edit_remote_warning": "Jiné vzdálené instance nemusí podporovat úpravy a nemusí přijmout nejnovější verzi vašeho příspěvku.", + "preview": "Náhled" }, "registration": { "bio": "O vás", @@ -105,8 +206,18 @@ "email_required": "nemůže být prázdný", "password_required": "nemůže být prázdné", "password_confirmation_required": "nemůže být prázdné", - "password_confirmation_match": "musí být stejné jako heslo" - } + "password_confirmation_match": "musí být stejné jako heslo", + "birthday_min_age": "musí být před nebo v {date}", + "birthday_required": "nemůže být ponecháno prázdné" + }, + "birthday_optional": "Datum narození (volitelné):", + "register": "Registrovat", + "reason": "Důvod pro registraci", + "reason_placeholder": "Tato instance schvaluje registrace manuálně,\nZdůvodněte administraci důvod registrace.", + "birthday": "Datum narození:", + "email_language": "V jakém jazyce chcete přijímat emaily z tohoto serveru?", + "email_optional": "Email (volitelný)", + "bio_optional": "Bio (volitelné)" }, "settings": { "app_name": "Název aplikace", @@ -136,7 +247,7 @@ "default_vis": "Výchozí rozsah viditelnosti", "delete_account": "Smazat účet", "delete_account_description": "Trvale smaže váš účet a všechny vaše příspěvky.", - "delete_account_error": "Při mazání vašeho účtu nastala chyba. Pokud tato chyba bude trvat, kontaktujte prosím admministrátora vaší instance.", + "delete_account_error": "Při mazání vašeho účtu nastala chyba. Pokud tato chyba přetrvává, kontaktujte prosím administrátora vaší instance.", "delete_account_instructions": "Pro potvrzení smazání účtu napište své heslo do pole níže.", "avatar_size_instruction": "Doporučená minimální velikost pro avatarové obrázky je 150x150 pixelů.", "export_theme": "Uložit přednastavení", @@ -152,7 +263,7 @@ "general": "Obecné", "hide_attachments_in_convo": "Skrývat přílohy v konverzacích", "hide_attachments_in_tl": "Skrývat přílohy v časové ose", - "max_thumbnails": "Maximální počet miniatur na příspěvek", + "max_thumbnails": "Maximální počet miniatur na příspěvek (prázdné = žádný limit)", "hide_isp": "Skrýt panel specifický pro instanci", "preload_images": "Přednačítat obrázky", "use_one_click_nsfw": "Otevírat citlivé přílohy pouze jedním kliknutím", @@ -181,7 +292,7 @@ "new_password": "Nové heslo", "notification_visibility": "Typy oznámení k zobrazení", "notification_visibility_follows": "Sledující", - "notification_visibility_likes": "Oblíbení", + "notification_visibility_likes": "Oblíbené", "notification_visibility_mentions": "Zmínky", "notification_visibility_repeats": "Zopakování", "no_rich_text_description": "Odstranit ze všech příspěvků formátování textu", @@ -237,18 +348,34 @@ "true": "ano" }, "notifications": "Oznámení", - "enable_web_push_notifications": "Povolit webová push oznámení", + "enable_web_push_notifications": "Povolit web push oznámení", "style": { "switcher": { "keep_color": "Ponechat barvy", "keep_shadows": "Ponechat stíny", "keep_opacity": "Ponechat průhlednost", "keep_roundness": "Ponechat kulatost", - "keep_fonts": "Keep fonts", + "keep_fonts": "Ponechat písma", "save_load_hint": "Možnosti „Ponechat“ dočasně ponechávají aktuálně nastavené možností při volení či nahrávání motivů, také tyto možnosti ukládají při exportování motivu. Pokud není žádné pole zaškrtnuto, uloží export motivu všechno.", "reset": "Resetovat", "clear_all": "Vymazat vše", - "clear_opacity": "Vymazat průhlednost" + "clear_opacity": "Vymazat průhlednost", + "keep_as_is": "Ponechat jak je", + "use_snapshot": "Stará verze", + "help": { + "migration_napshot_gone": "Z nějakého důvodu chyběl snímek, některé věci můžou vypadat jinak, než si pamatujete.", + "fe_upgraded": "Motiv engine PleromaFE byl aktualizován po aktualizaci verze.", + "future_version_imported": "Soubor, který jste importoval/a byl vytvořen pro novější verzi FE.", + "older_version_imported": "Soubor, který jste importoval/a byl vytvořen pro starší verzi FE.", + "v2_imported": "Soubor, který jste importoval/a byl vytvořen pro starší verzi FE. Snažíme se zachovat maximální kompatibilitu, ale může dojít k nekonzistenci.", + "snapshot_present": "Snímek motivu byl načten, takže všechny hodnoty byly přepsány. Místo toho můžete načíst skutečná data motivu.", + "upgraded_from_v2": "PleromaFE bylo aktualizováno, motiv může vypadat trochu jinak, než si pamatujete.", + "snapshot_missing": "V souboru nebyl žádný snímek motivu, takže může vypadat jinak, než bylo původně zamýšleno.", + "fe_downgraded": "Verze PleromaFE byla vrácena zpět.", + "migration_snapshot_ok": "Pro jistotu byl načten snímek motivu. Můžete zkusit načíst data motivu." + }, + "load_theme": "Načíst motiv", + "use_source": "Nová verze" }, "common": { "color": "Barva", @@ -283,7 +410,27 @@ "borders": "Okraje", "buttons": "Tlačítka", "inputs": "Vstupní pole", - "faint_text": "Vybledlý text" + "faint_text": "Vybledlý text", + "popover": "Popisy, menu, popovery", + "underlay": "Podklad", + "pressed": "Zmáčknuté", + "selectedPost": "Vybraný příspěvek", + "selectedMenu": "Vybraná položka menu", + "alert_warning": "Varování", + "alert_neutral": "Neutrální", + "toggled": "Přepnuto", + "disabled": "Vypnuto", + "tabs": "Karty", + "chat": { + "incoming": "Příchozí", + "border": "Okraj", + "outgoing": "Odchozí" + }, + "post": "Příspěvky/Bio uživatelů", + "wallpaper": "Tapeta", + "poll": "Graf ankety", + "icons": "Ikony", + "highlight": "Zvýrazněné prvky" }, "radii": { "_tab_label": "Kulatost" @@ -346,7 +493,252 @@ "checkbox": "Pročetl/a jsem podmínky používání", "link": "hezký malý odkaz" } - } + }, + "added_alias": "Přezdívka přidána.", + "emoji_reactions_scale": "Měřítko zvětšení reakcí", + "file_export_import": { + "backup_restore": "Záloha nastavení", + "errors": { + "file_too_new": "Nekompatibilní hlavní verze: {fileMajor}, tato verze PleromaFE (verze {feMajor}) je příliš stará", + "invalid_file": "Vybraný soubor není podporovaná záloha Pleroma nastavení. Žádné změny nebyli provedeny.", + "file_too_old": "Nekompatibilní verze: {fileMajor}, verze souboru je příliš stará a nepodporovaná (min. verze {feMajor})", + "file_slightly_new": "Menší verze je rozdílná, některé nastavení se nemusí načíst" + }, + "backup_settings": "Zálohovat nastavení do souboru", + "backup_settings_theme": "Zálohovat nastavení a motiv do souboru", + "restore_settings": "Obnovit nastavení ze souboru" + }, + "backup_failed": "Záloha selhala.", + "tree_fade_ancestors": "Zobrazit autory aktuálního příspěvku ve slabém textu", + "mention_link_display_full_for_remote": "jako celá jména pouze pro vzdálené uživatele (např. {'@'}foo{'@'}example.org)", + "autocomplete_select_first": "Automaticky vybrat prvního kandidáta, když výsledky automatického doplnění jsou dostupné", + "import_blocks_from_a_csv_file": "Importovat blokace z csv souboru", + "backup_running": "Tato záloha právě probíhá, zpracován záznam {number}. |Tato záloha právě probíhá, zpracováno {number} záznamů.", + "changed_email": "Email byl úspěšně změněn!", + "chatMessageRadius": "Zpráva chatu", + "confirm_dialogs_delete": "mazání příspěvku", + "disable_sticky_headers": "Nezanechávat záhlaví sloupců na horní část obrazovky", + "third_column_mode_postform": "Editor hlavního příspěvku a navigaci", + "columns": "Sloupce", + "sensitive_by_default": "Označit příspěvky jako citlivé ve výchozím stavu", + "domain_mutes": "Domény", + "fallback_language": "Záložní jazyk {index}:", + "primary_language": "Hlavní jazyk:", + "security": "Zabezpečení", + "enter_current_password_to_confirm": "Zadejte vaše současné heslo pro potvrzení vaší identity", + "post_look_feel": "Vzhled příspěvků", + "mention_links": "Odkazy zmínek", + "mfa": { + "confirm_and_enable": "Potvrdit a zapnout OTP", + "title": "Dvoufázová autentizace", + "scan": { + "title": "Skenovat", + "desc": "Pomocí vaší 2FA aplikace oskenujte QR kód, nebo zadejte klíč:", + "secret_code": "Klíč" + }, + "otp": "OTP", + "generate_new_recovery_codes": "Vygenerovat nové záložní kódy", + "setup_otp": "Nastavit OTP", + "wait_pre_setup_otp": "přednastavení OTP", + "waiting_a_recovery_codes": "Přijímám záložní kódy…", + "recovery_codes_warning": "Zapište nebo uložte si záložní kódy jelikož je znovu již neuvidíte. Pokud ztratíte přístup k vaší 2FA aplikace a záložním kódům nebudete mít možnost se přihlásit k vašemu účtu.", + "recovery_codes": "Záložní kódy.", + "warning_of_generate_new_codes": "Když vygenerujete nové záložní kódy, tak staré přestanou fungovat.", + "authentication_methods": "Autentizační metody", + "verify": { + "desc": "Pro zapnutí dvoufázové autentizace zadejte kód z vaší 2FA aplikace:" + } + }, + "remove_backup": "Odstranit", + "email_language": "Jazyk pro přijímání emailů ze serveru", + "block_export": "Export blokací", + "block_import": "Import blokací", + "block_import_error": "Chyba při importování blokací", + "mute_export": "Exportovat ztlumení", + "mute_export_button": "Exportovat vaše ztlumení jako csv soubor", + "wordfilter": "Filtr slov", + "user_profiles": "Profily uživatelů", + "use_at_icon": "Zobrazovat {'@'} jako ikonu namísto textu", + "notification_visibility_moves": "Migrace uživatelů", + "hide_followers_count_description": "Nezobrazovat počet sledujících uživatelů", + "reply_visibility_self_short": "Zobrazit odpovědi pouze sobě", + "third_column_mode_notifications": "Sloupec oznámení", + "useStreamingApi": "Přijímat příspěvky a oznámení v reálném čase", + "use_websockets": "Používat websockety (Aktualizace v reálném čase)", + "user_mutes": "Uživatelé", + "mention_link_display": "Zobrazit odkazy na zmínky", + "add_language": "Přidat záložní jazyk", + "account_backup": "Zálohování účtu", + "account_alias": "Přezdívky účtu", + "setting_server_side": "Toto nastavení je vázané na váš profil a ovlivňuje všechny vaše sezení a klienty", + "block_export_button": "Exportovat vaše blokace jako csv soubor", + "blocks_imported": "Blokace importovány! Jejich zpracování může chvíli trvat.", + "mute_import": "Importovat ztlumení", + "mute_import_error": "Chyba při importování ztlumení", + "mutes_imported": "Ztlumení importovány! Jejich zpracování může chvíli trvat.", + "account_backup_table_head": "Záloha", + "download_backup": "Stáhnout", + "import_mutes_from_a_csv_file": "Importovat ztlumení z csv souboru", + "account_backup_description": "Toto umožňuje stáhnout archiv vašeho účtu a vašich příspěvků, ale nemůžou být zpětně importovány do Pleroma účtu.", + "backup_not_ready": "Tato záloha není zatím připravená.", + "list_backups_error": "Chyba při získávání seznamu záloh: {error}", + "add_backup": "Vytvořit novou zálohu", + "bot": "Toto je účet robota", + "change_email": "Změnit email", + "change_email_error": "Nastala chyba při změně vašeho emailu.", + "confirm_dialogs": "Požádat o potvrzení při", + "confirm_dialogs_mute": "ztlumení uživatele", + "confirm_dialogs_logout": "odhlašování", + "confirm_dialogs_approve_follow": "schvalování sledujícího", + "confirm_dialogs_deny_follow": "odmítání sledujícího", + "confirm_dialogs_remove_follower": "odstraňování sledujícího", + "mutes_and_blocks": "Ztlumení a Blokace", + "account_alias_table_head": "Přezdívka", + "move_account": "Přesunout účet", + "birthday": { + "show_birthday": "Zobrazit moje datum narození", + "label": "Datum narození" + }, + "account_privacy": "Soukromí", + "notification_visibility_in_column": "Zobrazit ve sloupci/zásuvce oznámení", + "notification_visibility_reports": "Nahlášení", + "notification_visibility_emoji_reactions": "Reakce", + "notification_visibility_polls": "Konce anket, ve kterých jste hlasovali", + "notification_extra_tip": "Zobrazení tipu přizpůsobení pro další oznámení", + "notification_visibility_native_notifications": "Zobrazit nativní oznámení", + "notification_visibility_follow_requests": "Požadavky na sledování", + "mute_bot_posts": "Skrýt příspěvky od robotů", + "hide_bot_indication": "Skrýt indikaci účtů robotů v příspěvcích", + "auto_update": "Zobrazovat nové příspěvky automaticky", + "url": "URL", + "preview": "Náhled", + "profile_fields": { + "label": "Metadata profilu", + "name": "Štítek", + "value": "Obsah", + "add_field": "Přidat pole" + }, + "hide_favorites_description": "Nezobrazovat seznam oblíbených příspěvků (uživatelé stále budou notifikování)", + "right_sidebar": "Prohodit pořadí sloupců", + "hide_scrobbles": "Skrýt scrobbles", + "hide_shoutbox": "Skrýt shoutbox instance", + "new_email": "Nový email", + "notification_show_extra": "Zobrazit další oznámení ve sloupci oznámení", + "reply_visibility_following_short": "Zobrazit odpovědi mým sledujícím", + "search_user_to_block": "Hledat koho chcete zablokovat", + "search_user_to_mute": "Hledat koho chcete ztlumit", + "reset_avatar_confirm": "Opravdu chcete resetovat avatar?", + "tree_advanced": "Umožnit více flexibilní navigaci ve stromovém zobrazení", + "conversation_display_linear_quick": "Lineární zobrazení", + "max_depth_in_thread": "Maximální počet zobrazených úrovní ve vlákně ve výchozím stavu", + "add_backup_error": "Chyba při přidávání nové zálohy: {error}", + "added_backup": "Přidána nová záloha.", + "word_filter_and_more": "Filtr slov a další...", + "posts": "Příspěvky", + "reset_banner_confirm": "Opravdu chcete resetovat banner?", + "reset_background_confirm": "Opravdu chcete resetovat pozadí?", + "reset_avatar": "Resetovat avatar", + "reset_profile_background": "Resetovat pozadí profilu", + "reset_profile_banner": "Resetovat banner profilu", + "type_domains_to_mute": "Hledat domény ke ztlumení", + "virtual_scrolling": "Optimalizovat vykreslování časové osy", + "remove_language": "Odstranit", + "expert_mode": "Zobrazit pokročilé nastavení", + "save": "Uložit změny", + "setting_changed": "Nastavení je rozdílné od výchozího", + "lists_navigation": "Zobrazovat seznamy v navigaci", + "allow_following_move": "Povolit automatické sledování pokud se sledovaný účet přesune", + "confirm_dialogs_repeat": "opakování příspěvku", + "confirm_dialogs_unfollow": "zrušení sledování uživatele", + "confirm_dialogs_block": "blokování uživatele", + "list_aliases_error": "Chyba při zjišťování přezdívek: {error}", + "remove_alias": "Odstranit tuto přezdívku", + "new_alias_target": "Přidat novou přezdívku (např. {example})", + "add_alias_error": "Chyba při přidávání přezdívky: {error}", + "hide_list_aliases_error_action": "Zavřít", + "move_account_notes": "Pokud chcete přesunut účet jinam, musíte jít na cílový účet a přidat přezdívku ukazující na tento účet.", + "move_account_target": "Cílový účet (např. {example})", + "moved_account": "Účet přesunut.", + "move_account_error": "Chyba při přesouvání účtu: {error}", + "discoverable": "Umožnit objevení tohoto účtu ve výsledcích vyhledávání a v jiných službách", + "pad_emoji": "Přidat mezeru okolo emoji při přidávání emoji z výběru", + "emoji_reactions_on_timeline": "Zobrazit emoji reakce u příspěvků", + "hide_media_previews": "Schovat náhledy médií", + "hide_muted_posts": "Skrýt příspěvky od ztlumených uživatelů", + "hide_all_muted_posts": "Skrýt ztlumené uživatele", + "navbar_column_stretch": "Protáhnout navbar na šířku sloupců", + "always_show_post_button": "Vždy zobrazovat plovoucí tlačítko pro nový příspěvek", + "hide_wallpaper": "Skrýt pozadí instance", + "hide_wordfiltered_statuses": "Skrýt slovně filtrované příspěvky", + "hide_muted_threads": "Skrýt ztlumené vlákna", + "notification_extra_chats": "Zobrazit nepřečtené chaty", + "notification_extra_announcements": "Zobrazit nepřečtené oznámení", + "notification_extra_follow_requests": "Zobrazit nové požadavky na sledování", + "hide_follows_count_description": "Nezobrazovat počet sledovaných uživatelů", + "autohide_floating_post_button": "Automaticky skrýt tlačítko nového příspěvku (mobilní zařízení)", + "minimal_scopes_mode": "Minimalizovat možnosti rozsahu příspěvků", + "conversation_display": "Styl zobrazení konverzací", + "conversation_display_tree": "Stromové zobrazení", + "conversation_display_tree_quick": "Stromový styl", + "show_scrollbars": "Zobrazit posuvníky bočních sloupců", + "third_column_mode": "Pokud je volné místo, zobrazit třetí sloupec obsahující", + "third_column_mode_none": "Nikdy nezobrazovat třetí sloupec", + "column_sizes": "Velikost sloupců", + "column_sizes_sidebar": "Postranní panel", + "column_sizes_content": "Obsah", + "column_sizes_notifs": "Oznámení", + "conversation_display_linear": "Lineární styl", + "conversation_other_replies_button": "Zobrazit tlačítko ostatních odpovědí", + "conversation_other_replies_button_below": "Pod příspěvky", + "conversation_other_replies_button_inside": "Uvnitř příspěvků", + "mention_link_display_short": "vždy jako zkrácená jména (např. {'@'}foo)", + "mention_link_display_full": "vždy jako celá jména (např. {'@'}foo{'@'}example.org)", + "enable_web_push_always_show_tip": "Některé prohlížeče (Chromium, Chrome) vyžadují aby push zprávy vždy vytvořili oznámení, jinak obecné oznámení 'Tento web byl aktualizován na pozadí' je zobrazeno, povolte abyste zabránili tomuto oznámení, protože Chrome nejspíš skrývá push oznámení, pokud je panel zobrazen. Může mít za následek duplicitní oznámení v ostatních prohlížečích.", + "actor_type": "Tento účet je:", + "actor_type_description": "Když svůj účet označíte jako skupinu, bude automaticky opakovat všechny příspěvky, které ho zmíní.", + "actor_type_Person": "normální uživatel", + "actor_type_Service": "bot", + "actor_type_Group": "skupina", + "hide_actor_type_indication": "Skrýt označení typu účtu (bot, skupina atd.) v příspěvcích", + "mention_link_show_avatar": "Zobrazit avatar uživatele vedle odkazu", + "mention_link_show_avatar_quick": "Zobrazit avatar uživatele vedle zmínky", + "mention_link_fade_domain": "Zeslabit doménu (např {'@'}example.org v {'@'}foo{'@'}example.org)", + "fun": "Zábava", + "notification_mutes": "Pokud nechcete dostávat oznámení od specifický uživatelů, použijte funkci ztlumení.", + "more_settings": "Víc nastavení", + "user_popover_avatar_action_zoom": "Zvětšit avatar", + "user_popover_avatar_action_close": "Zavřít popover", + "notification_setting_annoyance": "Nepříjemnost", + "user_popover_avatar_action_open": "Otevřit profil", + "notification_setting_drawer_marks_as_seen": "Zavření zásuvky (na mobilu) označí všechny oznámení jako přečtené", + "notification_setting_ignore_inactionable_seen": "Ignorovat stav přečtení pro oznámení bez akce (oblíbené, opakování atd.)", + "notification_setting_unseen_at_top": "Zobrazovat nepřečtené oznámení nad ostatními", + "enable_web_push_always_show": "Vždy zobrazovat web push oznámení", + "notification_setting_privacy": "Soukromí", + "notification_setting_block_from_strangers": "Blokovat oznámení od uživatelů které nesledujete", + "notification_setting_hide_notification_contents": "Schovávat odesílatele a obsah push oznámení", + "notification_blocks": "Blokování uživatele zastaví všechny notifikace a také je odhlásí.", + "mention_link_use_tooltip": "Zobrazit kartu uživatele při kliknutí na zmínku", + "user_popover_avatar_overlay": "Zobrazit popover uživatele přes jeho avatar", + "greentext": "Vtipné šipky", + "mention_link_bolden_you": "Zvýraznit vaši zmínku", + "user_popover_avatar_action": "Popover akce při kliknutí na avatar", + "show_yous": "Zobrazit (Vy)", + "notification_setting_filters": "Filtry", + "notification_setting_ignore_inactionable_seen_tip": "Toto ve skutečnosti neoznačí tyto oznámení jako přečtené a stále o nich budete dostávat oznámení na počítači, pokud si tak vyberete", + "notification_setting_filters_chrome_push": "V některých prohlížečích (Chrome) nemusí být možné kompletně vyfiltrovat oznámení, pokud přijdou jako push oznámení", + "commit_value": "Uložit", + "reset_value": "Resetovat", + "reset_value_tooltip": "Resetovat koncept", + "hard_reset_value": "Tvrdý reset", + "version": { + "title": "Verze", + "backend_version": "Backend verze", + "frontend_version": "Frontend verze" + }, + "commit_value_tooltip": "Hodnota není uložena, stiskněte toto tlačítko pro potvrzení změn", + "hard_reset_value_tooltip": "Odstranit nastavení z úložiště a vynutit výchozí hodnotu", + "accent": "Akcentní barva" }, "time": { "day": "{0} day", @@ -357,8 +749,8 @@ "hours": "{0} hours", "hour_short": "{0}h", "hours_short": "{0}h", - "in_future": "in {0}", - "in_past": "{0} ago", + "in_future": "za {0}", + "in_past": "před {0}", "minute": "{0} minute", "minutes": "{0} minutes", "minute_short": "{0}min", @@ -367,8 +759,8 @@ "months": "{0} měs", "month_short": "{0} měs", "months_short": "{0} měs", - "now": "teď", - "now_short": "teď", + "now": "právě teď", + "now_short": "nyní", "second": "{0} second", "seconds": "{0} seconds", "second_short": "{0}s", @@ -380,7 +772,23 @@ "year": "{0} r", "years": "{0} l", "year_short": "{0}r", - "years_short": "{0}l" + "years_short": "{0}l", + "unit": { + "seconds_short": "{0}s", + "days": "{0} den | {0} dnů", + "days_short": "{0}d", + "hours": "{0} hodina | {0} hodin", + "hours_short": "{0}h", + "minutes": "{0} minuta | {0} minut", + "months": "{0} měsíc | {0} měsíců", + "months_short": "{0}mo", + "minutes_short": "{0}min", + "seconds": "{0} sekunda | {0} sekund", + "weeks": "{0} týden | {0} týdnů", + "weeks_short": "{0}w", + "years": "{0} rok | {0} roky", + "years_short": "{0}y" + } }, "timeline": { "collapse": "Zabalit", @@ -392,11 +800,60 @@ "show_new": "Zobrazit nové", "up_to_date": "Aktuální", "no_more_statuses": "Žádné další příspěvky", - "no_statuses": "Žádné příspěvky" + "no_statuses": "Žádné příspěvky", + "socket_reconnected": "Navázáno spojení v reálném čase", + "error": "Chyba při načítání časové osy: {0}", + "reload": "Načíst znovu", + "socket_broke": "Spojení v reálném čase ztraceno: CloseEvent code {0}" }, "status": { "reply_to": "Odpověď uživateli", - "replies_list": "Odpovědi:" + "replies_list": "Odpovědi:", + "many_attachments": "Příspěvek má {number} příloh(u)", + "collapse_attachments": "Sbalit přílohy", + "unpin": "Odepnout z profilu", + "thread_muted": "Vlákno ztlumeno", + "show_attachment_description": "Popis náhledu (otevřete přílohu pro celý popis)", + "move_down": "Posunout přílohu doprava", + "thread_show": "Zobrazit toto vlákno", + "pin": "Připnout na profil", + "mute_conversation": "Ztlumit konverzaci", + "thread_hide": "Skrýt toto vlákno", + "show_full_subject": "Zobrazit celý předmět", + "edited_at": "(naposledy upraveno {time})", + "repeat_confirm_accept_button": "Zopakovat", + "repeat_confirm_title": "Potvrzení zopakování", + "delete_error": "Chyba při mazání příspěvku: {0}", + "delete_confirm": "Opravdu chcete smazat tento příspěvek?", + "delete_confirm_title": "Potvrzení smazání", + "delete_confirm_accept_button": "Smazat", + "delete_confirm_cancel_button": "Ponechat", + "you": "(Vy)", + "hide_attachment": "Skrýt přílohu", + "remove_attachment": "Odstranit přílohu", + "attachment_stop_flash": "Zastavit Flash player", + "nsfw": "NSFW", + "repeat_confirm_cancel_button": "Neopakovat", + "favorites": "Oblíbené", + "repeats": "Opakovaní", + "repeat_confirm": "Opravdu chcete zopakovat tento příspěvek?", + "delete": "Smazat příspěvek", + "copy_link": "Kopírovat odkaz k příspěvku", + "external_source": "Externí zdroj", + "edit": "Upravit příspěvek", + "bookmark": "Přidat do záložek", + "unbookmark": "Odebrat ze záložek", + "mentions": "Zmínky", + "hide_full_subject": "Skrýt celý předmět", + "show_content": "Zobrazit obsah", + "hide_content": "Skrýt obsah", + "unmute_conversation": "Zrušit ztlumení konverzace", + "status_unavailable": "Příspěvek je nedostupný", + "status_deleted": "Tento příspěvek byl smazán", + "expand": "Rozbalit", + "show_all_attachments": "Zobrazit všechny přílohy", + "move_up": "Posunout přílohu doleva", + "open_gallery": "Otevřít galerii" }, "user_card": { "approve": "Schválit", @@ -406,7 +863,7 @@ "favorites": "Oblíbené", "follow": "Sledovat", "follow_sent": "Požadavek odeslán!", - "follow_progress": "Odeslílám požadavek…", + "follow_progress": "Odesílám požadavek…", "follow_unfollow": "Přestat sledovat", "followees": "Sledovaní", "followers": "Sledující", @@ -455,5 +912,270 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "about": { + "mrf": { + "federation": "Federace", + "keyword": { + "ftl_removal": "Odstranění z časové osy \"Celá známá síť\"", + "reject": "Odmítnout", + "replace": "Nahradit", + "is_replaced_by": "→", + "keyword_policies": "Zásady klíčových slov" + }, + "mrf_policies": "Povolené MRF zásady", + "simple": { + "instance": "Instance", + "reason": "Důvod", + "not_applicable": "N/A", + "accept": "Přijmout", + "accept_desc": "Tato instance přijímá zprávy pouze z následujících instancí:", + "reject": "Odmítnout", + "quarantine": "Karanténa", + "quarantine_desc": "Tato instance bude posílat pouze veřejné zprávy na tyto instance:", + "media_removal": "Odstranění médií", + "media_nsfw_desc": "Tato instance vynucuje média nastavené jako citlivé v příspěvcích z následujících instancí:", + "simple_policies": "Zásady specifické pro danou instanci", + "ftl_removal": "Odstranění z časové osy \"Celá známá síť\"", + "media_nsfw": "Vynutit média jako citlivé", + "reject_desc": "Tato instance nebude přijímat zprávy z následujících instancí:", + "media_removal_desc": "Tato instance odstraňuje média v příspěvcích z následujících instancí:", + "ftl_removal_desc": "Tato instance odstraňuje tyto instance z časové osy \"Celá známá síť\":" + }, + "mrf_policies_desc": "Zásady MRF mění chování federace této instance. Následující MRF zásady jsou povoleny:" + }, + "staff": "Personál" + }, + "exporter": { + "processing": "Zpracovávám, zanedlouho budete vyzváni ke stažení vašeho souboru", + "export": "Exportovat" + }, + "remote_user_resolver": { + "searching_for": "Hledám", + "error": "Nenalezeno." + }, + "polls": { + "multiple_choices": "Výběr více možností", + "expiry": "Doba ankety", + "add_poll": "Přidat anketu", + "add_option": "Přidat možnost", + "single_choice": "Výběr jediné možnosti", + "option": "Možnost", + "votes": "hlasy", + "people_voted_count": "{count} hlasoval/a | {count} voličů", + "votes_count": "{count} hlasovat | {count} hlasů", + "vote": "Hlasovat", + "type": "Typ ankety", + "expires_in": "Anketa končí za {0}", + "expired": "Anketa skončila před {0}", + "not_enough_options": "Příliš málo jedinečných možností v anketě" + }, + "interactions": { + "follows": "Nových sledujících", + "moves": "Uživatel migroval", + "load_older": "Načíst starší interakce", + "emoji_reactions": "Emoji reakce", + "reports": "Stížnosti", + "favs_repeats": "Opakované a oblíbené" + }, + "emoji": { + "unicode_groups": { + "animals-and-nature": "Zvířata a příroda", + "flags": "Vlajky", + "activities": "Aktivity", + "people-and-body": "Lidé a těla", + "food-and-drink": "Jídlo a pití", + "objects": "Objekty", + "smileys-and-emotion": "Smajlíky a emoce", + "symbols": "Symboly", + "travel-and-places": "Cestování a místa" + }, + "unicode": "Unicode emoji", + "load_all": "Načítání všech {emojiAmount} emoji", + "stickers": "Nálepky", + "emoji": "Emoji", + "keep_open": "Ponechat okno výběru otevřené", + "search_emoji": "Hledat emoji", + "add_emoji": "Vložit emoji", + "custom": "Vlastní emoji", + "load_all_hint": "Načteno prvních {saneAmount} emoji, načítání všech emoji může způsobit problémy s výkonem.", + "unpacked": "Rozbalené emoji", + "regional_indicator": "Regionální indikátor {letter}", + "hide_custom_emoji": "Skrýt vlastní emoji" + }, + "importer": { + "submit": "Odeslat", + "success": "Úspěšně importováno.", + "error": "Nastala chyba při importování ze souboru." + }, + "report": { + "reporter": "Nahlašující:", + "reported_user": "Nahlášený uživatel:", + "reported_statuses": "Nahlášené příspěvky:", + "notes": "Poznámky:", + "state": "Stav:", + "state_open": "Otevřeno", + "state_closed": "Uzavřeno", + "state_resolved": "Vyřešeno" + }, + "announcements": { + "mark_as_read_action": "Označit jako přečtené", + "page_header": "Oznámení", + "title": "Oznámení", + "post_form_header": "Vyvěsit oznámení", + "post_placeholder": "Zde napište obsah vašeho oznámení…", + "post_action": "Odeslat", + "post_error": "Chyba: {error}", + "close_error": "Zavřít", + "delete_action": "Smazat", + "start_time_prompt": "Čas začátku: ", + "end_time_prompt": "Čas ukončení: ", + "all_day_prompt": "Toto je celodenní akce", + "published_time_display": "Zveřejněno v {time}", + "start_time_display": "Začíná v {time}", + "end_time_display": "Končí v {time}", + "edit_action": "Upravit", + "submit_edit_action": "Odeslat", + "cancel_edit_action": "Zrušit", + "inactive_message": "Toto oznámení není aktivní" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "domain_mute_card": { + "mute": "Ztlumit", + "mute_progress": "Ztlumuji…", + "unmute": "Zrušit ztlumení", + "unmute_progress": "Ruším ztlumení…" + }, + "errors": { + "storage_unavailable": "Pleroma nemohla získat přístup k úložišti prohlížeče. Vaše přihlášení nebo lokální nastavení se neuloží a můžete narazit na neočekávané problémy. Zkuste povolit soubory cookies." + }, + "selectable_list": { + "select_all": "Vybrat vše" + }, + "admin_dash": { + "window_title": "Administrace", + "commit_all": "Uložit vše", + "tabs": { + "nodb": "Žádné nastavení v databázi", + "frontends": "Frontendy", + "instance": "Instance", + "limits": "Limity", + "emoji": "Emoji" + }, + "nodb": { + "heading": "Nastavení v databázi je vypnuto", + "documentation": "dokumentace", + "text2": "Většina konfiguračních možností nebude dostupná." + }, + "wip_notice": "Tento administrační panel je experimentální a v aktivní vývoji, {adminFeLink}.", + "old_ui_link": "staré administrační rozhraní je dostupné zde", + "reset_all": "Resetovat vše", + "frontend": { + "failure_installing_frontend": "Nepodařilo se nainstalovat frontend {version}: {reason}", + "reinstall": "Přeinstalovat", + "available_frontends": "Dostupné k instalaci", + "is_default": "(Výchozí)", + "versions": "Dostupné verze", + "build_url": "URL sestavení", + "install": "Instalovat", + "install_version": "Instalovat verzi {version}", + "more_install_options": "Více instalačních možností", + "more_default_options": "Více výchozích nastavení pro možnosti", + "set_default": "Nastavit výchozí", + "default_frontend": "Výchozí frontend", + "set_default_version": "Nastavit verzi {version} jako výchozí", + "repository": "Odkaz k repozitáři", + "is_default_custom": "(Výchozí, verze: {version})", + "success_installing_frontend": "Frontend {version} byl úspěšně nainstalován" + }, + "captcha": { + "native": "Nativní", + "kocaptcha": "KoCaptcha" + }, + "instance": { + "instance": "Informace o instanci", + "captcha_header": "CAPTCHA", + "restrict": { + "activities": "Přístup k příspěvkům a aktivitám", + "timelines": "Přístup k časovým osám", + "profiles": "Přístup k uživatelským profilům", + "header": "Omezit přístup pro anonymní návštěvníky" + }, + "registrations": "Registrace uživatelů", + "kocaptcha": "KoCaptcha nastavení" + }, + "limits": { + "posts": "Limity příspěvků", + "uploads": "Limity příloh", + "users": "Limity uživatelských profilů", + "arbitrary_limits": "Libovolné limity", + "profile_fields": "Limity profilových polí", + "user_uploads": "Limity médií profilů" + }, + "emoji": { + "global_actions": "Globální akce", + "reload": "Znovu načíst emoji", + "importFS": "Importovat emoji ze souborového systému", + "error": "Chyba: {0}", + "create_pack": "Vytvořit balíček", + "delete_pack": "Smazat balíček", + "new_pack_name": "Nový název balíčku", + "create": "Vytvořit", + "emoji_packs": "Emoji balíčky", + "remote_packs": "Vzdálené balíčky", + "do_list": "List", + "emoji_pack": "Emoji balíček", + "edit_pack": "Upravit balíček", + "description": "Popis", + "homepage": "Domovská stránka", + "fallback_src": "Záložní zdroj", + "fallback_sha256": "Záložní SHA256", + "share": "Sdílet", + "save": "Uložit", + "save_meta": "Uložit metadata", + "revert_meta": "Vrátit zpět metadata", + "delete": "Smazat", + "add_file": "Přidat soubor", + "adding_new": "Přidávání nových emoji", + "shortcode": "Zkratka", + "filename": "Jméno souboru", + "new_shortcode": "Zkrat, ponechte prázdné pro odvození", + "delete_confirm": "Opravdu chcete smazat {0}?", + "download_pack": "Stáhnout balíček", + "downloading_pack": "Stahování {0}", + "download": "Stáhnout", + "download_as_name": "Nové jméno", + "download_as_name_full": "Nové jméno, pro opakované použití ponechte prázdné", + "files": "Soubory", + "editing": "Upravování {0}", + "delete_title": "Smazat?", + "emoji_changed": "Neuložené změny emoji souborů, zkontrolujte zvýrazněné emoji", + "replace_warning": "Tímto se NAHRADÍ místní balíček se stejným jménem", + "metadata_changed": "Metadata jsou rozdílné od uložených", + "revert": "Vrátit zpět", + "new_filename": "Jméno souboru, ponechte prázdné pro odvození" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":background_image": { + "label": "Obrázek na pozadí", + "description": "Obrázek na pozadí (především používáno PleromaFE)" + }, + ":description_limit": { + "label": "Limit", + "description": "Limit počtu znaků pro popisy příloh" + }, + ":public": { + "label": "Instance je veřejná" + }, + ":limit_to_local_content": { + "label": "Limitovat vyhledávání pouze na místní obsah" + } + } + } + } } } diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -186,10 +186,11 @@ "edit_pinned": "Edit pinned items", "edit_finish": "Done editing", "mobile_sidebar": "Toggle mobile sidebar", - "mobile_notifications": "Open notifications", "mobile_notifications": "Open notifications (there are unread ones)", "mobile_notifications_close": "Close notifications", - "announcements": "Announcements" + "mobile_notifications_mark_as_seen": "Mark all as seen", + "announcements": "Announcements", + "quotes": "Quotes" }, "notifications": { "broken_favorite": "Unknown status, searching for it…", @@ -205,7 +206,14 @@ "migrated_to": "migrated to", "reacted_with": "reacted with {0}", "submitted_report": "submitted a report", - "poll_ended": "poll has ended" + "poll_ended": "poll has ended", + "unread_announcements": "{num} unread announcement | {num} unread announcements", + "unread_chats": "{num} unread chat | {num} unread chats", + "unread_follow_requests": "{num} new follow request | {num} new follow requests", + "configuration_tip": "You can customize what to display here in {theSettings}. {dismiss}", + "configuration_tip_settings": "the settings", + "configuration_tip_dismiss": "Do not show again", + "subscribed_status": "posted" }, "polls": { "add_poll": "Add poll", @@ -230,6 +238,7 @@ "search_emoji": "Search for an emoji", "add_emoji": "Insert emoji", "custom": "Custom emoji", + "hide_custom_emoji": "Hide custom emojis", "unpacked": "Unpacked emoji", "unicode": "Unicode emoji", "unicode_groups": { @@ -256,7 +265,8 @@ "emoji_reactions": "Emoji Reactions", "reports": "Reports", "moves": "User migrates", - "load_older": "Load older interactions" + "load_older": "Load older interactions", + "statuses": "Subscriptions" }, "post_status": { "edit_status": "Edit status", @@ -352,6 +362,11 @@ "remove_language": "Remove", "primary_language": "Primary language:", "fallback_language": "Fallback language {index}:", + "actor_type": "This account is:", + "actor_type_description": "Marking your account as a group will make it automatically repeat statuses that mention it.", + "actor_type_Person": "a normal user", + "actor_type_Service": "a bot", + "actor_type_Group": "a group", "app_name": "App name", "expert_mode": "Show advanced", "save": "Save changes", @@ -361,6 +376,20 @@ "enter_current_password_to_confirm": "Enter your current password to confirm your identity", "post_look_feel": "Posts Look & Feel", "mention_links": "Mention links", + "appearance": "Appearance", + "confirm_new_setting": "Confirm new setting?", + "confirm_new_question": "Does this look ok? Setting will be reverted in 10 seconds.", + "revert": "Revert", + "confirm": "Confirm", + "text_size": "Text and interface size", + "text_size_tip": "Use {0} for absolute values, {1} will scale with browser default text size.", + "text_size_tip2": "Values other than {0} might break some things and themes", + "emoji_size": "Emoji size", + "navbar_size": "Top bar size", + "panel_header_size": "Panel header size", + "visual_tweaks": "Minor visual tweaks", + "theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)", + "scale_and_layout": "Interface scale and layout", "mfa": { "otp": "OTP", "setup_otp": "Setup OTP", @@ -382,6 +411,14 @@ "desc": "To enable two-factor authentication, enter the code from your two-factor app:" } }, + "units": { + "time": { + "m": "minutes", + "s": "seconds", + "h": "hours", + "d": "days" + } + }, "lists_navigation": "Show lists in navigation", "allow_following_move": "Allow auto-follow when following account moves", "attachmentRadius": "Attachments", @@ -416,7 +453,6 @@ "added_backup": "Added a new backup.", "add_backup_error": "Error adding a new backup: {error}", "blocks_tab": "Blocks", - "bot": "This is a bot account", "btnRadius": "Buttons", "cBlue": "Blue (Reply, follow)", "cGreen": "Green (Retweet)", @@ -488,7 +524,10 @@ "hide_media_previews": "Hide media previews", "hide_muted_posts": "Hide posts of muted users", "mute_bot_posts": "Mute bot posts", - "hide_bot_indication": "Hide bot indication in posts", + "hide_actor_type_indication": "Hide actor type (bots, groups, etc.) indication in posts", + "hide_scrobbles": "Hide scrobbles", + "hide_scrobbles_after": "Hide scrobbles older than", + "mute_sensitive_posts": "Mute sensitive posts", "hide_all_muted_posts": "Hide muted posts", "max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)", "hide_isp": "Hide instance-specific panel", @@ -554,13 +593,23 @@ "posts": "Posts", "user_profiles": "User Profiles", "notification_visibility": "Types of notifications to show", + "notification_visibility_in_column": "Show in notifications column/drawer", + "notification_visibility_native_notifications": "Show a native notification", "notification_visibility_follows": "Follows", + "notification_visibility_follow_requests": "Follow requests", "notification_visibility_likes": "Favorites", "notification_visibility_mentions": "Mentions", "notification_visibility_repeats": "Repeats", + "notification_visibility_reports": "Reports", "notification_visibility_moves": "User Migrates", "notification_visibility_emoji_reactions": "Reactions", "notification_visibility_polls": "Ends of polls you voted in", + "notification_visibility_statuses": "Subscriptions", + "notification_show_extra": "Show extra notifications in the notifications column", + "notification_extra_chats": "Show unread chats", + "notification_extra_announcements": "Show unread announcements", + "notification_extra_follow_requests": "Show new follow requests", + "notification_extra_tip": "Show the customization tip for extra notifications", "no_rich_text_description": "Strip rich text formatting from all posts", "no_blocks": "No blocks", "no_mutes": "No mutes", @@ -613,6 +662,7 @@ "subject_line_email": "Like email: \"re: subject\"", "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", + "force_theme_recompilation_debug": "Disable theme cahe, force recompile on each boot (DEBUG)", "conversation_display": "Conversation display style", "conversation_display_tree": "Tree-style", "conversation_display_tree_quick": "Tree view", @@ -676,15 +726,59 @@ "greentext": "Meme arrows", "show_yous": "Show (You)s", "notifications": "Notifications", + "notification_setting_annoyance": "Annoyance", + "notification_setting_drawer_marks_as_seen": "Closing drawer (mobile) marks all notifications as read", + "notification_setting_ignore_inactionable_seen": "Ignore read state of inactionable notifications (likes, repeats etc)", + "notification_setting_ignore_inactionable_seen_tip": "This will not actually mark those notifications as read, and you'll still get desktop notifications about them if you chose so", + "notification_setting_unseen_at_top": "Show unread notifications above others", "notification_setting_filters": "Filters", + "notification_setting_filters_chrome_push": "On some browsers (chrome) it might be impossible to completely filter out notifications by type when they arrive by Push", "notification_setting_block_from_strangers": "Block notifications from users who you do not follow", "notification_setting_privacy": "Privacy", "notification_setting_hide_notification_contents": "Hide the sender and contents of push notifications", "notification_mutes": "To stop receiving notifications from a specific user, use a mute.", "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.", "enable_web_push_notifications": "Enable web push notifications", + "enable_web_push_always_show": "Always show web push notifications", + "enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.", "more_settings": "More settings", "style": { + "custom_theme_used": "(Custom theme)", + "themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.", + "appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI", + "update_preview": "Update preview", + "themes3": { + "define": "Override", + "hacks": { + "underlay_overrides": "Change underlay", + "underlay_override_mode_none": "Theme default", + "underlay_override_mode_opaque": "Replace with solid color", + "underlay_override_mode_transparent": "Remove entirely (might break some themes)", + "force_interface_roundness": "Override interface roundness/sharpness", + "forced_roundness_mode_disabled": "Use theme defaults", + "forced_roundness_mode_sharp": "Force sharp edges", + "forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges", + "forced_roundness_mode_round": "Force round edges" + }, + "font": { + "group-builtin": "Browser default fonts", + "builtin" : { + "serif": "Serif", + "sans-serif": "Sans-serif", + "monospace": "Monospace", + "inherit": "Unchanged" + }, + "group-local": "Locally installed fonts", + "local-unavailable1": "List of locally installed fonts unavailalbe", + "local-unavailable2": "Use manual entry to specify custom font", + "font_list_unavailable": "Couldn't get locally installed fonts: {error}", + "lookup_local_fonts": "Load list of fonts installed on this computer", + "enter_manually": "Enter font name family manually", + "entry": "Enter {fontFamily}", + "select": "Select font" + } + }, + "interface_font_user_override": "Override theme/browser font used", "switcher": { "keep_color": "Keep colors", "keep_shadows": "Keep shadows", @@ -808,7 +902,7 @@ "interface": "Interface", "input": "Input fields", "post": "Post text", - "postCode": "Monospaced text in a post (rich text)" + "monospace": "Monospaced text" }, "family": "Font name", "size": "Size (in px)", @@ -852,7 +946,8 @@ "nodb": "No DB Config", "instance": "Instance", "limits": "Limits", - "frontends": "Front-ends" + "frontends": "Front-ends", + "emoji": "Emoji" }, "nodb": { "heading": "Database config is disabled", @@ -875,7 +970,7 @@ "description": "Detailed setting for allowing/disallowing access to certain aspects of API. By default (indeterminate state) it will disallow if instance is not public, ticked checkbox means disallow access even if instance is public, unticked means allow access even if instance is private. Please note that unexpected behavior might happen if some settings are set, i.e. if profile access is disabled posts will show without profile information.", "timelines": "Timelines access", "profiles": "User profiles access", - "activities": "Statues/activities access" + "activities": "Statuses/activities access" } }, "limits": { @@ -900,12 +995,57 @@ "set_default": "Set default", "set_default_version": "Set version {version} as default", "wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.", - "default_frontend": "Default front-end", - "default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.", + "default_frontend": "Default frontend", + "default_frontend_tip": "Default frontend will be shown to all users. Currently there's no way to for a user to select personal frontend. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.", + "default_frontend_unavail": "Default frontend settings are not available, as this requires configuration in the database", "available_frontends": "Available for install", "failure_installing_frontend": "Failed to install frontend {version}: {reason}", "success_installing_frontend": "Frontend {version} successfully installed" }, + "emoji": { + "global_actions": "Global actions", + "reload": "Reload emoji", + "importFS": "Import emoji from filesystem", + "error": "Error: {0}", + "create_pack": "Create pack", + "delete_pack": "Delete pack", + "new_pack_name": "New pack name", + "create": "Create", + "emoji_packs": "Emoji packs", + "remote_packs": "Remote packs", + "do_list": "List", + "remote_pack_instance": "Remote pack instance", + "emoji_pack": "Emoji pack", + "edit_pack": "Edit pack", + "description": "Description", + "homepage": "Homepage", + "fallback_src": "Fallback source", + "fallback_sha256": "Fallback SHA256", + "share": "Share", + "save": "Save", + "save_meta": "Save metadata", + "revert_meta": "Revert metadata", + "delete": "Delete", + "revert": "Revert", + "add_file": "Add file", + "adding_new": "Adding new emoji", + "shortcode": "Shortcode", + "filename": "Filename", + "new_shortcode": "Shortcode, leave blank to infer", + "new_filename": "Filename, leave blank to infer", + "delete_confirm": "Are you sure you want to delete {0}?", + "download_pack": "Download pack", + "downloading_pack": "Downloading {0}", + "download": "Download", + "download_as_name": "New name", + "download_as_name_full": "New name, leave blank to reuse", + "files": "Files", + "editing": "Editing {0}", + "delete_title": "Delete?", + "metadata_changed": "Metadata different from saved", + "emoji_changed": "Unsaved emoji file changes, check highlighted emoji", + "replace_warning": "This will REPLACE the local pack of the same name" + }, "temp_overrides": { ":pleroma": { ":instance": { @@ -971,6 +1111,7 @@ "status": { "favorites": "Favorites", "repeats": "Repeats", + "quotes": "Quotes", "repeat_confirm": "Do you really want to repeat this status?", "repeat_confirm_title": "Repeat confirmation", "repeat_confirm_accept_button": "Repeat", @@ -999,6 +1140,7 @@ "external_source": "External source", "thread_muted": "Thread muted", "thread_muted_and_words": ", has words:", + "sensitive_muted": "Muting sensitive content", "show_full_subject": "Show full subject", "hide_full_subject": "Hide full subject", "show_content": "Show content", @@ -1035,7 +1177,9 @@ "hide_quote": "Hide the quoted status", "display_quote": "Display the quoted status", "invisible_quote": "Quoted status unavailable: {link}", - "more_actions": "More actions on this status" + "more_actions": "More actions on this status", + "loading": "Loading...", + "load_error": "Unable to load status: {error}" }, "user_card": { "approve": "Approve", @@ -1102,6 +1246,7 @@ "hide_repeats": "Hide repeats", "show_repeats": "Show repeats", "bot": "Bot", + "group": "Group", "birthday": "Born {birthday}", "admin_menu": { "moderation": "Moderation", diff --git a/src/i18n/eo.json b/src/i18n/eo.json @@ -121,7 +121,9 @@ "mobile_notifications_close": "Fermi sciigojn", "announcements": "Anoncoj", "search_close": "Fermi serĉujon", - "mobile_sidebar": "(Mal)ŝalti flankan breton por telefonoj" + "mobile_sidebar": "(Mal)ŝalti flankan breton por telefonoj", + "mobile_notifications_mark_as_seen": "Marki ĉion vidita", + "quotes": "Citoj" }, "notifications": { "broken_favorite": "Nekonata afiŝo, serĉante ĝin…", @@ -137,7 +139,13 @@ "follow_request": "volas vin aboni", "error": "Eraris akirado de sciigoj: {0}", "submitted_report": "sendis raporton", - "poll_ended": "enketo finiĝis" + "poll_ended": "enketo finiĝis", + "unread_chats": "{num} nelegita babilo | {num} nelegitaj babiloj", + "unread_follow_requests": "{num} nova abonpeto | {num} novaj abonpetoj", + "configuration_tip": "Vi povas ŝanĝi, kio montriĝos ĉi tie en {theSettings}. {dismiss}", + "configuration_tip_settings": "la agordoj", + "unread_announcements": "{num} nelegita anonco | {num} nelegitaj anoncoj", + "configuration_tip_dismiss": "Ne remontri plu" }, "post_status": { "new_status": "Afiŝi", @@ -172,12 +180,14 @@ "direct_warning_to_first_only": "Ĉi tiu afiŝo estas nur videbla al uzantoj menciitaj je la komenco de la mesaĝo.", "direct_warning_to_all": "Ĉi tiu afiŝo estos videbla al ĉiuj menciitaj uzantoj.", "media_description": "Priskribo de vidaŭdaĵo", - "post": "Afiŝo", + "post": "Afiŝi", "edit_remote_warning": "Aliaj foraj nodoj eble ne subtenas redaktadon, kaj ne povos ricevi pli novan version de via afiŝo.", "edit_unsupported_warning": "Pleroma ne subtenas redaktadon de mencioj aŭ enketoj.", "edit_status": "Redakti afiŝon", "content_type_selection": "Formo de afiŝo", - "scope_notice_dismiss": "Fermi ĉi tiun avizon" + "scope_notice_dismiss": "Fermi ĉi tiun avizon", + "reply_option": "Respondi al ĉi tiu afiŝo", + "quote_option": "Citi ĉi tiun afiŝon" }, "registration": { "bio": "Priskribo", @@ -699,7 +709,42 @@ "confirm_dialogs_approve_follow": "aprobo de abonanto", "confirm_dialogs_deny_follow": "malaprobo de abonanto", "confirm_dialogs_remove_follower": "forigo de abonanto", - "tree_fade_ancestors": "Montri responditojn de la nuna afiŝo per teksto malvigla" + "tree_fade_ancestors": "Montri responditojn de la nuna afiŝo per teksto malvigla", + "units": { + "time": { + "m": "minutoj", + "s": "sekundoj", + "h": "horoj", + "d": "tagoj" + } + }, + "url": "URL", + "emoji_reactions_scale": "Grandeco de reagoj", + "actor_type_Person": "ordinara uzanto", + "actor_type": "Ĉi tiu konto estas:", + "actor_type_description": "Se vi markos vian konton grupo, ĝi memage ripetos afiŝojn, kiuj mencios ĝin.", + "actor_type_Service": "roboto", + "actor_type_Group": "grupo", + "hide_actor_type_indication": "Kaŝi specon de aganto (roboto, grupo, ktp.) en afiŝoj", + "commit_value_tooltip": "Valoro ne estas konservita; premu ĉi tiun butonon por konfirmi viajn ŝanĝojn", + "add_language": "Aldoni rezervan lingvon", + "commit_value": "Konservi", + "force_theme_recompilation_debug": "Malŝalti haŭtan kaŝmemoron, devigi retradukon post ĉiu enlego (POR ERARSERĈADO)", + "fallback_language": "Rezerva lingvo {index}:", + "notification_extra_follow_requests": "Montri novajn abonpetojn", + "notification_extra_tip": "Montri agordan konsileton por ekstraj sciigoj", + "notification_show_extra": "Montri ekstrajn sciigojn en la sciiga kolumno", + "notification_extra_chats": "Montri nelegitajn babilojn", + "notification_extra_announcements": "Montri nelegitajn anoncojn", + "notification_setting_annoyance": "Ĝeno", + "mute_sensitive_posts": "Silentigi konsternajn afiŝojn", + "preview": "Antaŭrigardo", + "notification_visibility_native_notifications": "Montri indiĝenan sciigon", + "notification_visibility_follow_requests": "Abonpetoj", + "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" }, "timeline": { "collapse": "Maletendi", @@ -873,7 +918,9 @@ "symbols": "Simboloj", "travel-and-places": "Vojaĝoj kaj lokoj" }, - "regional_indicator": "Regiona marko {letter}" + "regional_indicator": "Regiona marko {letter}", + "unpacked": "Malpakitaj bildosignoj", + "hide_custom_emoji": "Kaŝi proprajn bildosignojn" }, "polls": { "not_enough_options": "Tro malmultaj unikaj elektebloj en la enketo", @@ -1188,5 +1235,79 @@ "cancel_edit_action": "Nuligi", "inactive_message": "Ĉi tiu anonco estas neaktiva", "post_form_header": "Afiŝi anoncon" + }, + "admin_dash": { + "frontend": { + "default_frontend": "Implicita fasado", + "install": "Instali", + "versions": "Disponeblaj versioj", + "install_version": "Instali version {version}", + "more_install_options": "Pli da elektebloj je instalo", + "more_default_options": "Pli da elektebloj je implicitaj agordoj", + "set_default": "Agordi implicita", + "reinstall": "Reinstali", + "default_frontend_tip": "Implicita fasado montriĝos al ĉiuj uzantoj. Ankoraŭ ne ekzistas maniero, kiel uzanto povas elekti propran fasadon. Se vi ŝaltos ion alian, ol [PleromaFE], vi verŝajne devos uzadi la malnovan kaj erareman [AdminFE] por agordi la nodon, ĝis ni anstataŭigos ĝin.", + "repository": "Ligilo al deponejo", + "is_default": "(Implicita)", + "is_default_custom": "(Implicita, versio: {version})", + "set_default_version": "Agordi version {version} implicita" + }, + "emoji": { + "download_as_name_full": "Nova nomo; lasu malplena por reuzi", + "download_as_name": "Nova nomo", + "reload": "Re-enlegi bildosignojn", + "importFS": "Enporti bildosignojn de dosiersistemo", + "error": "Eraro: {0}", + "create": "Krei", + "do_list": "Listo", + "delete": "Forigi", + "add_file": "Aldoni dosieron", + "filename": "Dosiernomo", + "files": "Dosieroj", + "save_meta": "Konservi pridatumojn", + "description": "Priskribo", + "homepage": "Hejmpaĝo", + "save": "Konservi" + }, + "tabs": { + "emoji": "Bildosignoj", + "frontends": "Fasadoj", + "instance": "Nodo", + "limits": "Limoj" + }, + "instance": { + "registrations": "Registriĝoj de uzantoj", + "instance": "Informoj pri nodo", + "restrict": { + "profiles": "Aliro al profiloj de uzantoj", + "header": "Limigi aliron por sennomaj vizitantoj", + "timelines": "Aliro al historioj" + }, + "access": "Aliro al nodo" + }, + "limits": { + "users": "Limoj de profiloj de uzantoj", + "profile_fields": "Limoj de kampoj de profiloj", + "user_uploads": "Limoj de vidaŭdaĵoj de profiloj", + "posts": "Limoj de afiŝoj", + "uploads": "Limoj de kunsendaĵoj", + "arbitrary_limits": "Arbitraj limoj" + }, + "nodb": { + "documentation": "dokumentaĵo" + }, + "window_title": "Administrado", + "wip_notice": "Ĉi tiu administra fasado estas eksperimenta kaj ankoraŭ prilaborata, {adminFeLink}.", + "old_ui_link": "malnova administra fasado disponeblas tie ĉi", + "temp_overrides": { + ":pleroma": { + ":instance": { + ":public": { + "label": "Nodo estas publika" + } + } + } + }, + "commit_all": "Konservi ĉion" } } diff --git a/src/i18n/fr.json b/src/i18n/fr.json @@ -90,7 +90,11 @@ "heading": { "totp": "Authentification à double-facteur", "recovery": "Récupération de l'authentification à double-facteur" - } + }, + "logout_confirm_title": "Confirmation de déconnexion", + "logout_confirm": "Souhaitez-vous vous déconnecter ?", + "logout_confirm_accept_button": "Déconnexion", + "logout_confirm_cancel_button": "Ne pas se déconnecter" }, "media_modal": { "previous": "Précédent", @@ -110,7 +114,7 @@ "timeline": "Flux personnel", "twkn": "Réseau connu", "user_search": "Recherche de comptes", - "who_to_follow": "Suggestion de suivit", + "who_to_follow": "Suggestion de suivi", "preferences": "Préférences", "search": "Recherche", "administration": "Administration", @@ -124,7 +128,10 @@ "edit_pinned": "Éditer les éléments agrafés", "edit_finish": "Édition terminée", "mobile_sidebar": "(Dés)activer le panneau latéral", - "mobile_notifications_close": "Fermer les notifications" + "mobile_notifications_close": "Fermer les notifications", + "search_close": "Fermer la barre de recherche", + "announcements": "Annonces", + "mobile_notifications_mark_as_seen": "Marquer tout comme vu" }, "notifications": { "broken_favorite": "Message inconnu, recherche en cours…", @@ -140,7 +147,13 @@ "follow_request": "veut vous suivre", "error": "Erreur de chargement des notifications : {0}", "poll_ended": "Sondage terminé", - "submitted_report": "Rapport envoyé" + "submitted_report": "Rapport envoyé", + "unread_announcements": "{num} annonce non lue | {num} annonces non lues", + "unread_chats": "{num} message non lu | {num} messages non lus", + "configuration_tip_settings": "les préférences", + "unread_follow_requests": "{num} nouvelle demande de suivi | {num} nouvelles demandes de suivi", + "configuration_tip": "Vous pouvez personnaliser ce qui est affiché ici dans {theSettings}. {dismiss}", + "configuration_tip_dismiss": "Ne plus montrer" }, "interactions": { "favs_repeats": "Partages et favoris", @@ -154,7 +167,7 @@ "new_status": "Poster un nouveau statut", "account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.", "account_not_locked_warning_link": "verrouillé", - "attachments_sensitive": "Marquer les pièce-jointes comme sensible", + "attachments_sensitive": "Marquer les pièces jointes comme sensible", "content_type": { "text/plain": "Texte brut", "text/html": "HTML", @@ -183,9 +196,13 @@ "preview": "Prévisualisation", "media_description": "Description de la pièce-jointe", "post": "Post", - "edit_status": "Éditer le status", + "edit_status": "Éditer le statut", "edit_remote_warning": "Des instances distantes pourraient ne pas supporter l'édition et seront incapables de recevoir la nouvelle version de votre post.", - "edit_unsupported_warning": "Pleroma ne supporte pas l'édition de mentions ni de sondages." + "edit_unsupported_warning": "Pleroma ne supporte pas l'édition de mentions ni de sondages.", + "reply_option": "Répondre à ce statut", + "quote_option": "Citer ce statut", + "scope_notice_dismiss": "Fermer ce message", + "content_type_selection": "Format du statut" }, "registration": { "bio": "Biographie", @@ -205,14 +222,18 @@ "email_required": "ne peut pas être laissé vide", "password_required": "ne peut pas être laissé vide", "password_confirmation_required": "ne peut pas être laissé vide", - "password_confirmation_match": "doit être identique au mot de passe" + "password_confirmation_match": "doit être identique au mot de passe", + "birthday_min_age": "doit être le ou avant le {date}", + "birthday_required": "ne peut pas être vide" }, "reason_placeholder": "Cette instance modère les inscriptions manuellement.\nExpliquer ce qui motive votre inscription à l'administration.", "reason": "Motivation d'inscription", "register": "Enregistrer", "email_language": "Dans quelle langue voulez-vous recevoir les emails du server ?", "bio_optional": "Biographie (optionnelle)", - "email_optional": "Courriel (optionnel)" + "email_optional": "Courriel (optionnel)", + "birthday": "Anniversaire :", + "birthday_optional": "Anniversaire (optionnel) :" }, "selectable_list": { "select_all": "Tout selectionner" @@ -684,7 +705,64 @@ "use_websockets": "Utiliser les websockets (mises à jour en temps réel)", "user_popover_avatar_action_zoom": "Zoomer sur l'avatar", "user_popover_avatar_action_open": "Ouvrir le profil", - "conversation_display_tree_quick": "Vue arborescente" + "conversation_display_tree_quick": "Vue arborescente", + "emoji_reactions_scale": "Taille des réactions", + "backup_running": "Cette sauvegarde est en cours, {number} enregistrement effectué. | Cette sauvegarde est en cours, {number} enregistrements effectués.", + "backup_failed": "Cette sauvegarde a échoué.", + "autocomplete_select_first": "Sélectionner automatiquement la première occurrence lorsque les résultats de l'autocomplétion sont disponibles", + "confirm_dialogs_unfollow": "arrête de suivre un utilisateur", + "confirm_dialogs_repeat": "reposte un statut", + "actor_type": "Ce compte est :", + "actor_type_Person": "un utilisateur normal", + "actor_type_Service": "un robot", + "actor_type_Group": "un groupe", + "confirm_dialogs_logout": "à la déconnexion", + "confirm_dialogs_approve_follow": "accepte un nouvel abonné", + "confirm_dialogs_deny_follow": "refuse un nouvel abonné", + "confirm_dialogs_remove_follower": "supprime un abonné", + "actor_type_description": "En marquant votre compte comme un groupe, vous répétez automatiquement les statuts qui le mentionnent.", + "add_language": "Ajouter une langue de remplacement", + "remove_language": "Supprimer", + "primary_language": "Langue principale :", + "fallback_language": "Langue de remplacement {index} :", + "confirm_dialogs": "Demande de confirmation quand", + "confirm_dialogs_block": "bloque un utilisateur", + "confirm_dialogs_mute": "mute un utilisateur", + "confirm_dialogs_delete": "supprime un statut", + "url": "URL", + "preview": "Aperçu", + "reset_value": "Réinitialiser", + "hard_reset_value_tooltip": "Supprime le réglage du stockage, force l'utilisation de la valeur par défaut", + "reset_value_tooltip": "Réinitialiser le brouillon", + "hard_reset_value": "Remise à zéro", + "hide_actor_type_indication": "Cacher le type (robots, groupes, etc.) dans les status", + "notification_extra_follow_requests": "Afficher les nouvelles demandes de suivi", + "user_popover_avatar_action": "Action du clic sur l'avatar", + "user_popover_avatar_action_close": "Fermer la fenêtre contextuelle", + "notification_setting_ignore_inactionable_seen": "Ignorer les status de lecture des notifications non actionnables (favoris, répétitions, etc)", + "notification_setting_ignore_inactionable_seen_tip": "Ceci ne marquera pas ces notifications comme lues, et vous recevrez encore les notifications de bureau si vous le décidez", + "notification_setting_unseen_at_top": "Afficher les notifications non lues au-dessus des autres", + "notification_setting_filters_chrome_push": "Sur certains navigateurs (chrome), il peut être impossible de filtrer complètement les notifications par type lorsqu'elles arrivent", + "enable_web_push_always_show": "Toujours afficher les notifications web", + "commit_value": "Sauvegarder", + "hide_scrobbles": "Masquer les scrobbles", + "notification_setting_annoyance": "Agacement", + "notification_setting_drawer_marks_as_seen": "Fermer le tiroir marque toutes les notifications comme lues (mobile)", + "commit_value_tooltip": "Les valeurs ne sont pas sauvegardées, appuyez sur ce bouton pour soumettre vos changements", + "birthday": { + "show_birthday": "Afficher mon anniversaire", + "label": "Anniversaire" + }, + "notification_visibility_native_notifications": "Afficher une notification native", + "notification_visibility_follow_requests": "Demandes de suivi", + "notification_visibility_reports": "Rapports", + "notification_extra_chats": "Afficher les discussions non lues", + "notification_extra_announcements": "Afficher les annonces non lues", + "notification_extra_tip": "Afficher les astuces de personnalisation pour les notifications extras", + "enable_web_push_always_show_tip": "Certains navigateurs (Chromium, Chrome) exigent que les messages push donnent toujours lieu à une notification, sinon le message générique \"Le site web a été mis à jour en arrière-plan\" s'affiche ; activez cette option pour empêcher l'affichage de cette notification, car Chrome semble masquer les notifications push si l'onglet est au centre de l'attention. Cela peut entraîner l'affichage de notifications en double sur d'autres navigateurs.", + "user_popover_avatar_overlay": "Afficher la fenêtre contextuelle sur l'avatar de l'utilisateur", + "notification_visibility_in_column": "Afficher la colonne / le tiroir de notifications", + "notification_show_extra": "Afficher les extras dans la colonne de notifications" }, "timeline": { "collapse": "Fermer", @@ -758,7 +836,20 @@ "show_all_conversation": "Montrer tout le fil ({numStatus} autre message) | Montrer tout le fil ({numStatus} autre messages)", "edit": "Éditer le status", "edited_at": "(dernière édition {time})", - "status_history": "Historique du status" + "status_history": "Historique du status", + "delete_error": "Erreur de suppression du statut : {0}", + "repeat_confirm": "Voulez-vous réellement reposter ce statut ?", + "reaction_count_label": "{num} personne a réagi | {num} personnes ont réagi", + "repeat_confirm_cancel_button": "Ne pas reposter", + "hide_quote": "Masquer les status cités", + "display_quote": "Afficher les status cités", + "invisible_quote": "Citation de statut non disponible : {link}", + "delete_confirm_title": "Confirmer la suppression", + "more_actions": "Plus d'action sur ce statut", + "delete_confirm_cancel_button": "Conserver", + "repeat_confirm_title": "Confirmer reposte", + "repeat_confirm_accept_button": "Reposter", + "delete_confirm_accept_button": "Supprimer" }, "user_card": { "approve": "Accepter", @@ -828,7 +919,39 @@ "edit_profile": "Éditer le profil", "deactivated": "Désactivé", "follow_cancel": "Annuler la requête", - "remove_follower": "Retirer l'abonné·e" + "remove_follower": "Retirer l'abonné·e", + "remove_follower_confirm_accept_button": "Supprimer", + "approve_confirm_cancel_button": "Ne pas approuver", + "block_confirm_accept_button": "Bloquer", + "mute_confirm_title": "Confirmation de mise en sourdine", + "block_confirm_cancel_button": "Ne pas bloquer", + "unfollow_confirm": "Voulez-vous vraiment arrêter de suivre {user} ?", + "unfollow_confirm_accept_button": "Ne plus suivre", + "birthday": "Né(e) le {birthday}", + "edit_note": "Éditer note", + "edit_note_apply": "Appliquer", + "edit_note_cancel": "Abandonner", + "note": "Note", + "group": "Groupe", + "unfollow_confirm_title": "Confirmer l'arrêt de suivi", + "block_confirm_title": "Confirmer le blocage", + "deny_confirm_accept_button": "Refuser", + "deny_confirm_cancel_button": "Ne pas refuser", + "deny_confirm": "Voulez-vous refuser la demande de suivi de {user} ?", + "deny_confirm_title": "Refuser la confirmation", + "remove_follower_confirm_cancel_button": "Conserver", + "mute_duration_prompt": "Mettre cet utilisateur en sourdine pour (0 pour une durée indéterminée) :", + "remove_follower_confirm_title": "Confirmation de suppression d'utilisateur", + "note_blank": "(Aucun)", + "mute_confirm": "Voulez-vous vraiment mettre {user} en sourdine ?", + "mute_confirm_accept_button": "Mettre en sourdine", + "mute_confirm_cancel_button": "Ne pas mettre en sourdine", + "remove_follower_confirm": "Voulez-vous vraiment supprimer {user} de vos abonnés ?", + "approve_confirm_accept_button": "Approuver", + "approve_confirm": "Voulez-vous approuver la demande de suivi de {user} ?", + "block_confirm": "Voulez-vous vraiment bloquer {user} ?", + "approve_confirm_title": "Approuver confirmation", + "unfollow_confirm_cancel_button": "Ne pas arrêter le suivi" }, "user_profile": { "timeline_title": "Flux du compte", @@ -857,7 +980,10 @@ "add_reaction": "Ajouter une réaction", "accept_follow_request": "Accepter la demande de suivit", "reject_follow_request": "Rejeter la demande de suivit", - "bookmark": "Favori" + "bookmark": "Favori", + "autocomplete_available": "{number} résultat est disponible. Utilisez les touches haut et bas pour naviguer à l'intérieur. | {number} résultats sont disponibles. Utilisez les touches haut et bas pour naviguer à l'intérieur.", + "toggle_expand": "Développer ou réduire la notification pour afficher le message dans son intégralité", + "toggle_mute": "Développer ou réduire la notification pour révéler le contenu en sourdine" }, "upload": { "error": { @@ -950,7 +1076,9 @@ "symbols": "Symboles", "travel-and-places": "Voyages & lieux" }, - "regional_indicator": "Indicateur régional {letter}" + "regional_indicator": "Indicateur régional {letter}", + "unpacked": "Émojis non catégorisés", + "hide_custom_emoji": "Masquer les émojis personnalisés" }, "remote_user_resolver": { "error": "Non trouvé.", @@ -1012,7 +1140,7 @@ "person_talking": "{count} personnes discutant", "hashtags": "Mot-dièses", "people_talking": "{count} personnes discutant", - "no_results": "Aucun résultats", + "no_results": "Aucun résultat", "no_more_results": "Pas de résultats supplémentaires", "load_more": "Charger plus de résultats" }, @@ -1083,7 +1211,8 @@ "update_changelog_here": "Liste compète des changements", "art_by": "Œuvre par {linkToArtist}", "big_update_content": "Nous n'avons pas fait de nouvelle version depuis un moment, les choses peuvent vous paraitre différentes de vos habitudes.", - "update_bugs": "Veuillez rapporter les problèmes sur {pleromaGitlab}, comme beaucoup de changements on été fait, même si nous testons entièrement et utilisons la version de dévelopement nous-même, nous avons pu en louper. Les retours et suggestions sont bienvenues sur ce que vous avez pu rencontrer, ou sur comment améliorer Pleroma (BE) et Pleroma-FE." + "update_bugs": "Veuillez rapporter les problèmes sur {pleromaGitlab}, comme beaucoup de changements on été fait, même si nous testons entièrement et utilisons la version de dévelopement nous-même, nous avons pu en louper. Les retours et suggestions sont bienvenues sur ce que vous avez pu rencontrer, ou sur comment améliorer Pleroma (BE) et Pleroma-FE.", + "big_update_title": "Soyez indulgent avec nous" }, "unicode_domain_indicator": { "tooltip": "Ce domaine contient des caractères non ascii." @@ -1097,5 +1226,158 @@ "state_open": "Ouvert", "state_closed": "Fermé", "state_resolved": "Résolut" + }, + "announcements": { + "page_header": "Annonces", + "title": "Annonce", + "mark_as_read_action": "Marquer comme lu", + "post_form_header": "Faire une annonce", + "post_placeholder": "Écrivez le contenu de l'annonce ici...", + "post_action": "Envoyer", + "post_error": "Erreur : {error}", + "close_error": "Fermer", + "delete_action": "Supprimer", + "start_time_prompt": "Heure de début : ", + "end_time_prompt": "Heure de fin : ", + "all_day_prompt": "L'événement dure toute la journée", + "inactive_message": "Cette annonce n'est pas active", + "published_time_display": "Publié le {time}", + "start_time_display": "Démarre à {time}", + "end_time_display": "Se termine à {time}", + "edit_action": "Modifier", + "submit_edit_action": "Envoyer", + "cancel_edit_action": "Annuler" + }, + "admin_dash": { + "frontend": { + "success_installing_frontend": "Installation réussie de l'interface {version}", + "failure_installing_frontend": "Échec de l'installation de l'interface {version} : {reason}", + "default_frontend_unavail": "Les paramètres de l'interface ne sont pas disponibles, ils doivent être configurés dans la base de données", + "build_url": "Construction URL", + "reinstall": "Réinstaller", + "repository": "Lien du dépôt", + "versions": "Versions disponibles", + "default_frontend_tip": "L'interface par défaut sera affichée à tous les utilisateurs. Si vous décidez de quitter PleromaFE, vous devrez utiliser l'ancienne AdminFE buguée pour configurer votre instance jusqu'à ce que nous la remplacions.", + "is_default": "(Défaut)", + "is_default_custom": "(Défaut, version : {version})", + "install": "Installation", + "install_version": "Installation de la version {version}", + "more_install_options": "Plus d'options d'installation", + "more_default_options": "Plus d'options de paramétrages par défaut", + "set_default": "Définir la valeur par défaut", + "set_default_version": "Définir la version {version} comme version par défaut", + "wip_notice": "Veuillez noter que cette section est en cours de développement et que certaines fonctionnalités de l'interface ne sont pas implémentées côté serveur.", + "default_frontend": "Interface par défaut", + "available_frontends": "Disponible pour installation" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":public": { + "label": "Cette instance est publique", + "description": "En désactivant cette option, toutes les API ne seront accessibles qu'aux utilisateurs connectés, ce qui rendra les chronologies publiques et fédérées inaccessibles aux visiteurs anonymes." + }, + ":limit_to_local_content": { + "label": "Limitez la recherche au contenu local", + "description": "Désactive la recherche globale sur le réseau pour les utilisateurs non authentifiés (par défaut), tous les utilisateurs ou aucun" + }, + ":description_limit": { + "label": "Limite", + "description": "Limite de nombre de caractères pour la description des fichiers joints" + }, + ":background_image": { + "description": "Image de fond (principalement utilisé par PleromaFE)", + "label": "Image de fond d'écran" + } + } + } + }, + "tabs": { + "emoji": "Émoji", + "limits": "Limites", + "frontends": "Interfaces", + "instance": "Instance", + "nodb": "Pas de configuration de base de données" + }, + "instance": { + "kocaptcha": "Réglages KoCaptcha", + "access": "Accès à l'instance", + "restrict": { + "header": "Restreindre l'accès aux visiteurs anonymes", + "profiles": "Accès aux profils d'utilisateur", + "activities": "Accès aux status/activités", + "description": "Paramètre détaillé permettant d'autoriser/interdire l'accès à certains aspects de l'API. Par défaut (état indéterminé), l'accès est interdit si l'instance n'est pas publique ; si la case est cochée, l'accès est interdit même si l'instance est publique ; si la case n'est pas cochée, l'accès est autorisé même si l'instance est privée. Veuillez noter qu'un comportement inattendu peut se produire si certains paramètres sont définis, par exemple si l'accès au profil est désactivé, les messages s'afficheront sans les informations relatives au profil.", + "timelines": "Accès aux flux" + }, + "registrations": "Inscription des utilisateurs", + "captcha_header": "CAPTCHA", + "instance": "Informations sur l'instance" + }, + "emoji": { + "global_actions": "Actions globales", + "reload": "Recharger les émojis", + "importFS": "Importer les émojis depuis le système de fichiers", + "error": "Erreur : {0}", + "create_pack": "Créer un pack", + "delete_pack": "Supprimer un paquet", + "new_pack_name": "Renommer le pack", + "create": "Créer", + "emoji_packs": "Pack d'émojis", + "remote_packs": "Packs d'autres instances", + "do_list": "Liste", + "remote_pack_instance": "Instance du pack", + "emoji_pack": "Pack d'émoji", + "edit_pack": "Modifier le pack", + "description": "Description", + "homepage": "Page d'accueil", + "fallback_src": "Source de remplacement", + "fallback_sha256": "Remplacement SHA256", + "share": "Partager", + "save": "Sauvegarder", + "save_meta": "Sauvegarder les métadonnées", + "revert_meta": "Annuler métadonnées", + "delete": "Supprimer", + "revert": "Revenir en arrière", + "add_file": "Ajouter un fichier", + "adding_new": "Ajouter un nouvel émoji", + "shortcode": "Shortcode", + "filename": "Nom du fichier", + "new_filename": "Nom de fichier, laisser blanc pour inférer", + "delete_confirm": "Êtes-vous sûr de vouloir supprimer {0} ?", + "download_pack": "Télécharger pack", + "downloading_pack": "Télécharge {0}", + "download": "Téléchargement", + "download_as_name": "Nouveau nom", + "download_as_name_full": "Nouveau nom, laissez blanc pour réutiliser le précédent", + "files": "Fichiers", + "editing": "Édition de {0}", + "delete_title": "Supprimer ?", + "metadata_changed": "Métadonnées différentes de celles sauvegardées", + "emoji_changed": "Modifications du fichier émoji non sauvegardées, vérifier l'émoji surligné", + "replace_warning": "Vous allez REMPLACER le pack local qui porte ce nom" + }, + "window_title": "Administration", + "nodb": { + "heading": "La configuration de base de données est désactivée", + "documentation": "documentation", + "text2": "La majorité des options de configuration ne seront pas disponibles.", + "text": "Vous devez modifier les fichiers de configuration du serveur pour que {property} soit définie à {value}, plus de détails dans la {documentation}." + }, + "limits": { + "arbitrary_limits": "Limites arbitraires", + "posts": "Limites des statuts", + "uploads": "Limites des pièces jointes", + "users": "Limites du profil d'utilisateur", + "profile_fields": "Limites des champs du profile", + "user_uploads": "Limites des médias du profil" + }, + "captcha": { + "native": "Natif", + "kocaptcha": "KoCaptcha" + }, + "wip_notice": "Ce tableau de bord d'administration est expérimental et en cours de développement, {adminFeLink}.", + "old_ui_link": "L'ancien espace d'administration est disponible ici", + "reset_all": "Tout réinitialiser", + "commit_all": "Tout sauvegarder" } } diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json @@ -163,7 +163,8 @@ "search_close": "けんさくバーをとじる", "edit_nav_mobile": "ナビゲーションバーのせっていをかえる", "mobile_sidebar": "モバイルのサイドバーをきりかえる", - "edit_finish": "へんしゅうをおわりにする" + "edit_finish": "へんしゅうをおわりにする", + "mobile_notifications_mark_as_seen": "ぜんぶ みたことにする" }, "notifications": { "broken_favorite": "ステータスがみつかりません。さがしています…", @@ -179,7 +180,13 @@ "migrated_to": "インスタンスを、ひっこしました", "reacted_with": "{0} でリアクションしました", "poll_ended": "とうひょうが、おわりました", - "submitted_report": "つうほうしました" + "submitted_report": "つうほうしました", + "unread_announcements": "まだ よんでいない おしらせが {num}こ あります", + "configuration_tip_settings": "せってい", + "configuration_tip_dismiss": "つぎは ひょうじしない", + "unread_chats": "よんでいない チャットが {num}こ あります", + "unread_follow_requests": "フォローリクエストが {num}こ あります", + "configuration_tip": "ここに ひょうじする ものを {theSettings}で へんこうできます。 {dismiss}" }, "polls": { "add_poll": "とうひょうをはじめる", @@ -218,7 +225,8 @@ "symbols": "きごう", "travel-and-places": "りょこう・ばしょ" }, - "regional_indicator": "ばしょをしめすきごう {letter}" + "regional_indicator": "ばしょをしめすきごう {letter}", + "unpacked": "アンパックされた えもじ" }, "stickers": { "add_sticker": "ステッカーをふやす" @@ -269,7 +277,9 @@ "preview": "プレビュー", "preview_empty": "なにもありません", "empty_status_error": "とうこうないようを、にゅうりょくしてください", - "scope_notice_dismiss": "このつうちをとじる" + "scope_notice_dismiss": "このつうちをとじる", + "reply_option": "この ステータスに へんしんする", + "quote_option": "この ステータスを いんようする" }, "registration": { "bio": "プロフィール", @@ -324,7 +334,7 @@ "warning_of_generate_new_codes": "あたらしいリカバリーコードをつくったら、ふるいコードはつかえなくなります。", "recovery_codes": "リカバリーコード。", "waiting_a_recovery_codes": "バックアップコードをうけとっています…", - "recovery_codes_warning": "コードをかきうつすか、ひとにみられないところにセーブしてください。そうでなければ、あなたはこのコードをふたたびみることはできません。もしあなたが、2FAアプリのアクセスをうしなって、なおかつ、リカバリーコードもおもいだせないならば、あなたはあなたのアカウントから、しめだされます。", + "recovery_codes_warning": "コードを かきうつすか、 ほかのひとが みれないところに ほぞんしてください。 そうしないと、 あなたは このコードを にどと みることができません。 もし あなたが 2FAアプリに アクセスできなくなり、 リカバリーコードも おもいだせないなら、 あなたは あなたの アカウントに はいれなくなります。", "authentication_methods": "にんしょうメソッド", "scan": { "title": "スキャン", @@ -697,9 +707,9 @@ "import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする", "reset_avatar": "アバターをリセットする", "remove_language": "とりのぞく", - "primary_language": "いちばんわかることば:", + "primary_language": "さいしょに つかう ことば:", "add_language": "よびとしてつかうことばを、ついかする", - "fallback_language": "よびとしてつかうことば {index}:", + "fallback_language": "よびとして つかう ことば {index}:", "lists_navigation": "ナビゲーションにリストをひょうじする", "account_alias": "アカウントのエイリアス", "mention_link_display_full": "いつも、ながいなまえをひょうじする (れい: {'@'}hoge{'@'}example.org)", @@ -797,7 +807,11 @@ "virtual_scrolling": "タイムラインのレンダリングをよくする", "use_at_icon": "{'@'} きごうを、もじのかわりに、アイコンでひょうじする", "mention_link_display_short": "いつも、みじかいなまえにする (れい: {'@'}hoge)", - "mention_link_display": "メンションのリンクをひょうじするけいしき" + "mention_link_display": "メンションのリンクをひょうじするけいしき", + "url": "URL", + "preview": "プレビュー", + "emoji_reactions_scale": "リアクションを なんばいの おおきさで ひょうじするか", + "autocomplete_select_first": "じどうほかんが あれば、 さいしょの ものを じどうで えらぶ" }, "time": { "day": "{0}日", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json @@ -4,36 +4,37 @@ }, "exporter": { "export": "エクスポート", - "processing": "処理中です。処理が完了すると、ファイルをダウンロードするよう指示があります" + "processing": "処理中です。処理が完了すると、ファイルのダウンロードが開始します" }, "features_panel": { "chat": "チャット", "gopher": "Gopher", - "media_proxy": "メディアプロクシ", + "media_proxy": "メディアプロキシ", "scope_options": "公開範囲選択", - "text_limit": "文字の数", + "text_limit": "文字数制限", "title": "有効な機能", "who_to_follow": "おすすめユーザー", "upload_limit": "ファイルサイズの上限", - "pleroma_chat_messages": "Pleroma チャット" + "pleroma_chat_messages": "Pleroma チャット", + "shout": "Shoutbox" }, "finder": { - "error_fetching_user": "ユーザー検索がエラーになりました", + "error_fetching_user": "ユーザーの取得に失敗しました", "find_user": "ユーザーを探す" }, "general": { "apply": "適用", "submit": "送信", - "more": "続き", - "generic_error": "エラーになりました", - "optional": "省略可", + "more": "もっと", + "generic_error": "エラーが発生しました", + "optional": "任意", "show_more": "もっと見る", "show_less": "たたむ", "cancel": "キャンセル", "disable": "無効", "enable": "有効", "confirm": "確認", - "verify": "検査", + "verify": "検証", "peek": "隠す", "close": "閉じる", "dismiss": "無視", @@ -46,7 +47,21 @@ }, "flash_security": "Flashコンテンツが任意の命令を実行させることにより、コンピューターが危険にさらされることがあります。", "flash_fail": "Flashコンテンツの読み込みに失敗しました。コンソールで詳細を確認できます。", - "flash_content": "(試験的機能)クリックしてFlashコンテンツを再生します。" + "flash_content": "(試験的機能) クリックしてFlashコンテンツを再生します。", + "yes": "はい", + "no": "いいえ", + "scroll_to_top": "最上部へスクロール", + "unpin": "ピン留めを外す", + "pin": "ピン留めする", + "scope_in_timeline": { + "direct": "ダイレクト", + "private": "フォロワー限定", + "public": "パブリック", + "unlisted": "アンリステッド" + }, + "generic_error_message": "エラーが発生しました: {0}", + "never_show_again": "二度と表示しない", + "undo": "取り消す" }, "image_cropper": { "crop_picture": "画像を切り抜く", @@ -57,7 +72,7 @@ "importer": { "submit": "送信", "success": "正常にインポートされました。", - "error": "このファイルをインポートするとき、エラーが発生しました。" + "error": "ファイルのインポート中にエラーが発生しました。" }, "login": { "login": "ログイン", @@ -69,30 +84,36 @@ "username": "ユーザー名", "hint": "会話に加わるには、ログインしてください", "authentication_code": "認証コード", - "enter_recovery_code": "リカバリーコードを入力してください", - "enter_two_factor_code": "2段階認証コードを入力してください", + "enter_recovery_code": "リカバリーコードを入力", + "enter_two_factor_code": "二段階認証コードを入力", "recovery_code": "リカバリーコード", "heading": { - "totp": "2段階認証", - "recovery": "2段階リカバリー" - } + "totp": "二段階認証", + "recovery": "二段階認証リカバリー" + }, + "logout_confirm": "本当にログアウトしますか?", + "logout_confirm_accept_button": "ログアウト", + "logout_confirm_cancel_button": "ログアウトしない", + "logout_confirm_title": "ログアウトの確認" }, "media_modal": { - "previous": "前", - "next": "次" + "previous": "前へ", + "next": "次へ", + "hide": "メディアビューアを閉じる", + "counter": "{current} / {total}" }, "nav": { "about": "このインスタンスについて", "back": "戻る", "chat": "ローカルチャット", "friend_requests": "フォローリクエスト", - "mentions": "通知", - "interactions": "インタラクション", + "mentions": "メンション", + "interactions": "通知", "dms": "ダイレクトメッセージ", "public_tl": "公開タイムライン", "timeline": "タイムライン", "twkn": "すべてのネットワーク", - "user_search": "ユーザーを探す", + "user_search": "ユーザー検索", "search": "検索", "who_to_follow": "おすすめユーザー", "preferences": "設定", @@ -100,21 +121,38 @@ "bookmarks": "ブックマーク", "timelines": "タイムライン", "chats": "チャット", - "home_timeline": "ホームタイムライン" + "home_timeline": "ホームタイムライン", + "mobile_notifications_mark_as_seen": "すべて既読にする", + "search_close": "検索バーを閉じる", + "lists": "リスト", + "edit_nav_mobile": "ナビゲーションバーを編集", + "edit_pinned": "ピン留めを編集", + "edit_finish": "完了", + "mobile_notifications": "通知を開く (未読あり)", + "mobile_notifications_close": "通知を閉じる", + "announcements": "お知らせ" }, "notifications": { "broken_favorite": "ステータスが見つかりません。探しています…", - "favorited_you": "あなたのステータスがお気に入りされました", + "favorited_you": "ステータスがお気に入りされました", "followed_you": "フォローされました", - "load_older": "古い通知をみる", + "load_older": "古い通知を読み込む", "notifications": "通知", "read": "読んだ!", - "repeated_you": "あなたのステータスがリピートされました", + "repeated_you": "ステータスがリピートされました", "no_more_notifications": "通知はありません", "reacted_with": "{0} でリアクションしました", "migrated_to": "インスタンスを引っ越しました", - "follow_request": "あなたをフォローしたいです", - "error": "通知の取得に失敗しました: {0}" + "follow_request": "あなたをフォローしたがっています", + "error": "通知の取得に失敗しました: {0}", + "poll_ended": "投票結果が確定しました", + "configuration_tip_dismiss": "二度と表示しない", + "unread_announcements": "未読のお知らせが{num}件あります | 未読のお知らせが{num}件あります", + "unread_chats": "未読のチャットが{num}件あります | 未読のチャットが{num}件あります", + "unread_follow_requests": "フォローリクエストが{num}件来ています | フォローリクエストが{num}件来ています", + "configuration_tip": "ここに表示する通知の種類は{theSettings}にて変更することができます。 {dismiss}", + "submitted_report": "通報が送信されました", + "configuration_tip_settings": "設定" }, "polls": { "add_poll": "投票を追加", @@ -128,20 +166,22 @@ "expiry": "投票期間", "expires_in": "投票は {0} で終了します", "expired": "投票は {0} 前に終了しました", - "not_enough_options": "相異なる選択肢が不足しています", + "not_enough_options": "選択肢が少なすぎます", "votes_count": "{count} 票 | {count} 票", - "people_voted_count": "{count} 人投票 | {count} 人投票" + "people_voted_count": "{count}人が投票しました | {count}人が投票しました" }, "emoji": { "stickers": "ステッカー", "emoji": "絵文字", - "keep_open": "ピッカーを開いたままにする", + "keep_open": "絵文字ピッカーを開いたままにする", "search_emoji": "絵文字を検索", "add_emoji": "絵文字を挿入", "custom": "カスタム絵文字", "unicode": "Unicode絵文字", "load_all": "全 {emojiAmount} 絵文字を読み込む", - "load_all_hint": "最初の {saneAmount} 絵文字を読み込みました、全て読み込むと重くなる可能性があります。" + "load_all_hint": "最初の {saneAmount} 件の絵文字を読み込みました。すべて読み込むとパフォーマンスに影響を与える可能性があります。", + "unpacked": "パック外の絵文字", + "hide_custom_emoji": "カスタム絵文字を表示しない" }, "stickers": { "add_sticker": "ステッカーを追加" @@ -149,30 +189,32 @@ "interactions": { "favs_repeats": "リピートとお気に入り", "follows": "新しいフォロワー", - "load_older": "古いインタラクションを見る", - "moves": "ユーザーの引っ越し" + "load_older": "古い通知を読み込む", + "moves": "ユーザーの引っ越し", + "emoji_reactions": "絵文字リアクション", + "reports": "通報" }, "post_status": { "new_status": "投稿する", - "account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でも、フォロワー限定のステータスを読むことができます。", - "account_not_locked_warning_link": "ロックされたアカウント", - "attachments_sensitive": "ファイルをNSFWにする", + "account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でもフォロワー限定のステータスを読むことができます。", + "account_not_locked_warning_link": "鍵アカウント", + "attachments_sensitive": "ファイルを閲覧注意に設定する", "content_type": { "text/plain": "プレーンテキスト", "text/html": "HTML", "text/markdown": "Markdown", "text/bbcode": "BBCode" }, - "content_warning": "説明 (省略可)", + "content_warning": "注釈 (任意)", "default": "羽田空港に着きました。", - "direct_warning_to_all": "この投稿は、メンションされたすべてのユーザーが、見ることができます。", - "direct_warning_to_first_only": "この投稿は、メッセージの冒頭でメンションされたユーザーだけが、見ることができます。", + "direct_warning_to_all": "この投稿は、メンションされたすべてのユーザーが閲覧できます。", + "direct_warning_to_first_only": "この投稿は、メッセージの冒頭でメンションされたユーザーだけが閲覧できます。", "direct_warning": "このステータスは、メンションされたユーザーだけが、読むことができます。", "posting": "投稿", "scope_notice": { - "public": "この投稿は、誰でも見ることができます", - "private": "この投稿は、あなたのフォロワーだけが、見ることができます", - "unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません" + "public": "この投稿は誰でも閲覧できます", + "private": "この投稿はフォロワーのみ閲覧できます", + "unlisted": "この投稿は、公開タイムラインとすべてのネットワークには表示されません" }, "scope": { "direct": "ダイレクト: メンションされたユーザーのみに届きます", @@ -180,22 +222,29 @@ "public": "パブリック: 公開タイムラインに届きます", "unlisted": "アンリステッド: 公開タイムラインに届きません" }, - "media_description_error": "メディアのアップロードに失敗しました。もう一度お試しください", + "media_description_error": "メディアのアップデートに失敗しました。もう一度お試しください", "empty_status_error": "投稿内容を入力してください", "preview_empty": "何もありません", "preview": "プレビュー", "media_description": "メディアの説明", - "post": "投稿" + "post": "投稿", + "edit_status": "ステータスを編集", + "reply_option": "このステータスに返信する", + "quote_option": "このステータスを引用する", + "edit_remote_warning": "他のインスタンスは投稿の編集に対応していないかもしれません。その場合、編集した内容は伝わりません。", + "edit_unsupported_warning": "Pleromaは、メンションと投票の編集に対応していません。", + "scope_notice_dismiss": "このメッセージを閉じる", + "content_type_selection": "投稿形式" }, "registration": { "bio": "プロフィール", - "email": "Eメール", - "fullname": "スクリーンネーム", + "email": "メールアドレス", + "fullname": "表示名", "password_confirm": "パスワードの確認", "registration": "登録", - "token": "招待トークン", + "token": "招待コード", "captcha": "CAPTCHA", - "new_captcha": "文字が読めないときは、画像をクリックすると、新しい画像になります", + "new_captcha": "文字が読めない場合、画像をクリックすると新しい画像が表示されます", "username_placeholder": "例: lain", "fullname_placeholder": "例: 岩倉玲音", "bio_placeholder": "例:\nこんにちは。私は玲音。\n私はアニメのキャラクターで、日本の郊外に住んでいます。私をWiredで見たことがあるかもしれません。", @@ -205,11 +254,18 @@ "email_required": "必須", "password_required": "必須", "password_confirmation_required": "必須", - "password_confirmation_match": "パスワードが違います" + "password_confirmation_match": "パスワードが一致しません", + "birthday_required": "必須", + "birthday_min_age": "{date} 以降のユーザーは登録できません" }, - "reason_placeholder": "このインスタンスは、新規登録を手動で受け付けています。\n登録したい理由を、インスタンスの管理者に教えてください。", - "reason": "登録するための目的", - "register": "登録" + "reason_placeholder": "このインスタンスは、新規登録を手動で承認しています。\n登録したい理由をインスタンスの管理者に教えてください。", + "reason": "登録を希望する理由", + "register": "登録", + "email_language": "このサーバーからのメールをどの言語で受け取りますか?", + "bio_optional": "プロフィール (任意)", + "email_optional": "メールアドレス (任意)", + "birthday": "誕生日:", + "birthday_optional": "誕生日 (任意):" }, "selectable_list": { "select_all": "すべて選択" @@ -223,44 +279,44 @@ "setup_otp": "OTPのセットアップ", "wait_pre_setup_otp": "OTPのプリセット", "confirm_and_enable": "OTPの確認と有効化", - "title": "2段階認証", + "title": "二段階認証", "generate_new_recovery_codes": "新しいリカバリーコードを生成", "warning_of_generate_new_codes": "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。", "recovery_codes": "リカバリーコード。", "waiting_a_recovery_codes": "バックアップコードを受信しています…", - "recovery_codes_warning": "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。", + "recovery_codes_warning": "リカバリーコードをどこか安全な場所に書き留めてください。このコードは二度と表示されません。二段階認証アプリへのアクセスを失い、リカバリーコードも紛失した場合、二度とアカウントにログインできなくなります。", "authentication_methods": "認証方法", "scan": { "title": "スキャン", - "desc": "あなたの2段階認証アプリを使って、このQRコードをスキャンするか、テキストキーを入力してください:", + "desc": "二段階認証アプリでQRコードを読み取るか、テキストキーを入力してください:", "secret_code": "キー" }, "verify": { - "desc": "2段階認証を有効にするには、あなたの2段階認証アプリのコードを入力してください:" + "desc": "二段階認証を有効にするには、二段階認証アプリに表示されたコードを入力してください:" } }, "attachmentRadius": "ファイル", "attachments": "ファイル", - "avatar": "アバター", - "avatarAltRadius": "通知のアバター", - "avatarRadius": "アバター", + "avatar": "アイコン", + "avatarAltRadius": "通知内のアイコン", + "avatarRadius": "アイコン", "background": "バックグラウンド", "bio": "プロフィール", "block_export": "ブロックのエクスポート", "block_export_button": "ブロックをCSVファイルにエクスポートする", "block_import": "ブロックのインポート", "block_import_error": "ブロックのインポートに失敗しました", - "blocks_imported": "ブロックをインポートしました! 実際に処理されるまでに、しばらく時間がかかります。", + "blocks_imported": "ブロックがインポートされました。処理には時間がかかる場合があります。", "blocks_tab": "ブロック", "btnRadius": "ボタン", "cBlue": "返信とフォロー", "cGreen": "リピート", "cOrange": "お気に入り", "cRed": "キャンセル", - "change_password": "パスワードを変える", - "change_password_error": "パスワードを変えることが、できなかったかもしれません。", - "changed_password": "パスワードが、変わりました!", - "collapse_subject": "説明のある投稿をたたむ", + "change_password": "パスワードを変更", + "change_password_error": "パスワードの変更中にエラーが発生しました。", + "changed_password": "パスワードが変更されました!", + "collapse_subject": "注釈のついた投稿をたたむ", "composing": "投稿", "confirm_new_password": "新しいパスワードの確認", "current_avatar": "現在のアバター", @@ -268,52 +324,52 @@ "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": "アカウント削除の確認のため、パスワードを入力してください。", "discoverable": "検索などのサービスでこのアカウントを見つけることを許可する", - "avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。", - "pad_emoji": "ピッカーから絵文字を挿入するとき、絵文字の両側にスペースを入れる", - "export_theme": "保存", + "avatar_size_instruction": "アイコン画像のサイズは150x150以上を推奨します。", + "pad_emoji": "絵文字ピッカーから絵文字を挿入するとき、絵文字の前後に空白を挿入する", + "export_theme": "ファイルにテーマを出力", "filtering": "フィルタリング", - "filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください", + "filtering_explanation": "これらの単語を含むステータスはミュートされます。(1行に1単語)", "follow_export": "フォローのエクスポート", "follow_export_button": "エクスポート", "follow_export_processing": "お待ちください。まもなくファイルをダウンロードできます。", "follow_import": "フォローのインポート", - "follow_import_error": "フォローのインポートがエラーになりました", - "follows_imported": "フォローがインポートされました! 少し時間がかかるかもしれません。", + "follow_import_error": "フォローのインポートに失敗しました", + "follows_imported": "フォローがインポートされました。処理には時間がかかる場合があります。", "foreground": "フォアグラウンド", "general": "全般", - "hide_attachments_in_convo": "スレッドのファイルを隠す", - "hide_attachments_in_tl": "タイムラインのファイルを隠す", - "hide_muted_posts": "ミュートしているユーザーの投稿を隠す", - "max_thumbnails": "投稿に含まれるサムネイルの最大数", - "hide_isp": "インスタンス固有パネルを隠す", + "hide_attachments_in_convo": "スレッド内のファイルを表示しない", + "hide_attachments_in_tl": "タイムラインのファイルを表示しない", + "hide_muted_posts": "ミュートしているユーザーの投稿を表示しない", + "max_thumbnails": "投稿に表示するサムネイルの最大数 (空にすると無制限)", + "hide_isp": "インスタンス固有パネルを表示しない", "preload_images": "画像を先読みする", - "use_one_click_nsfw": "NSFWなファイルを1クリックで開く", - "hide_post_stats": "投稿の統計を隠す (例: お気に入りの数)", - "hide_user_stats": "ユーザーの統計を隠す (例: フォロワーの数)", - "hide_filtered_statuses": "フィルターされた投稿を隠す", + "use_one_click_nsfw": "閲覧注意なファイルを1クリックで開く", + "hide_post_stats": "投稿の統計を表示しない (例: お気に入りの数)", + "hide_user_stats": "ユーザーの統計を表示しない (例: フォロワーの数)", + "hide_filtered_statuses": "フィルタリングされた投稿を表示しない", "import_blocks_from_a_csv_file": "CSVファイルからブロックをインポートする", "import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする", - "import_theme": "ロード", - "inputRadius": "インプットフィールド", + "import_theme": "ファイルからテーマを読み込む", + "inputRadius": "入力欄", "checkboxRadius": "チェックボックス", "instance_default": "(デフォルト: {value})", "instance_default_simple": "(デフォルト)", "interface": "インターフェース", "interfaceLanguage": "インターフェースの言語", - "invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマは変更されませんでした。", - "limited_availability": "あなたのブラウザではできません", + "invalid_theme_imported": "非対応の形式のテーマファイルです。テーマは変更されませんでした。", + "limited_availability": "非対応のブラウザです", "links": "リンク", - "lock_account_description": "あなたが認めた人だけ、あなたのアカウントをフォローできる", - "loop_video": "ビデオを繰り返す", - "loop_video_silent_only": "音のないビデオだけ繰り返す", + "lock_account_description": "フォローを承認制にする", + "loop_video": "動画をループ再生する", + "loop_video_silent_only": "音声のない動画のみループ再生する", "mutes_tab": "ミュート", - "play_videos_in_modal": "ビデオをメディアビューアーで見る", - "use_contain_fit": "画像のサムネイルを、切り抜かない", + "play_videos_in_modal": "動画をメディアビューアで再生する", + "use_contain_fit": "画像のサムネイルを切り抜かない", "name": "名前", "name_bio": "名前とプロフィール", "new_password": "新しいパスワード", @@ -322,55 +378,55 @@ "notification_visibility_likes": "お気に入り", "notification_visibility_mentions": "メンション", "notification_visibility_repeats": "リピート", - "no_rich_text_description": "リッチテキストを使わない", - "no_blocks": "ブロックはありません", - "no_mutes": "ミュートはありません", - "hide_follows_description": "フォローしている人を見せない", - "hide_followers_description": "フォロワーを見せない", - "hide_follows_count_description": "フォローしている人の数を見せない", - "hide_followers_count_description": "フォロワーの数を見せない", - "show_admin_badge": "\"管理者\"のバッジを見せる", - "show_moderator_badge": "\"モデレーター\"のバッジを見せる", - "nsfw_clickthrough": "NSFWなファイルを隠す", + "no_rich_text_description": "投稿のテキスト装飾を無効化する", + "no_blocks": "ブロックしたユーザーはいません", + "no_mutes": "ミュートしたユーザーはいません", + "hide_follows_description": "フォロー欄を非公開にする", + "hide_followers_description": "フォロワー欄を非公開にする", + "hide_follows_count_description": "フォロー数を非公開にする", + "hide_followers_count_description": "フォロワー数を非公開にする", + "show_admin_badge": "プロフィールに「管理者」バッジを表示する", + "show_moderator_badge": "プロフィールに「モデレーター」バッジを表示する", + "nsfw_clickthrough": "閲覧注意なファイルを隠す", "oauth_tokens": "OAuthトークン", "token": "トークン", "refresh_token": "トークンを更新", - "valid_until": "まで有効", + "valid_until": "有効期限", "revoke_token": "取り消す", "panelRadius": "パネル", - "pause_on_unfocused": "タブにフォーカスがないときストリーミングを止める", + "pause_on_unfocused": "タブにフォーカスがないとき、タイムラインの自動更新を止める", "presets": "プリセット", "profile_background": "プロフィールの背景", "profile_banner": "プロフィールのバナー", "profile_tab": "プロフィール", - "radii_help": "インターフェースの丸さを設定する", - "replies_in_timeline": "タイムラインのリプライ", - "reply_visibility_all": "すべてのリプライを見る", - "reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る", - "reply_visibility_self": "私に宛てられたリプライを見る", - "autohide_floating_post_button": "新しい投稿ボタンを自動的に隠す (モバイル)", + "radii_help": "インターフェースの角丸を設定する (ピクセル単位)", + "replies_in_timeline": "タイムライン上の返信", + "reply_visibility_all": "すべての返信を表示する", + "reply_visibility_following": "自分、もしくはフォローしているユーザー宛ての返信のみを表示する", + "reply_visibility_self": "自分に宛てられた返信のみを表示する", + "autohide_floating_post_button": "投稿ボタンを自動的に隠す (モバイル)", "saving_err": "設定を保存できませんでした", "saving_ok": "設定を保存しました", "search_user_to_block": "ブロックしたいユーザーを検索", "search_user_to_mute": "ミュートしたいユーザーを検索", "security_tab": "セキュリティ", - "scope_copy": "返信するとき、公開範囲をコピーする (DMの公開範囲は、常にコピーされます)", - "minimal_scopes_mode": "公開範囲選択オプションを最小にする", - "set_new_avatar": "新しいアバターを設定する", - "set_new_profile_background": "新しいプロフィールのバックグラウンドを設定する", - "set_new_profile_banner": "新しいプロフィールバナーを設定する", + "scope_copy": "返信の公開範囲を返信先に合わせる", + "minimal_scopes_mode": "公開範囲選択オプションを最小化する", + "set_new_avatar": "アイコンを設定する", + "set_new_profile_background": "プロフィールの背景を設定する", + "set_new_profile_banner": "プロフィールのバナーを設定する", "settings": "設定", - "subject_input_always_show": "サブジェクトフィールドをいつでも表示する", - "subject_line_behavior": "返信するときサブジェクトをコピーする", - "subject_line_email": "メール風: \"re: サブジェクト\"", - "subject_line_mastodon": "マストドン風: そのままコピー", + "subject_input_always_show": "注釈欄を常に表示する", + "subject_line_behavior": "返信するとき、返信先の注釈をコピーする", + "subject_line_email": "メール風: \"re: 注釈\"", + "subject_line_mastodon": "Mastodon風: そのままコピー", "subject_line_noop": "コピーしない", - "post_status_content_type": "投稿のコンテントタイプ", - "stop_gifs": "カーソルを重ねたとき、GIFを動かす", - "streaming": "上までスクロールしたとき、自動的にストリーミングする", + "post_status_content_type": "デフォルトの投稿形式", + "stop_gifs": "GIFを自動再生しない", + "streaming": "上までスクロールしたとき、自動でタイムラインを更新する", "text": "文字", "theme": "テーマ", - "theme_help": "カラーテーマをカスタマイズできます。", + "theme_help": "カラーコード(#rrggbb)を使用してカラーテーマをカスタマイズできます。", "theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、色と透明度をオーバーライドできます。「すべてクリア」ボタンを押すと、すべてのオーバーライドをやめます。", "theme_help_v2_2": "バックグラウンドとテキストのコントラストを表すアイコンがあります。マウスをホバーすると、詳しい説明が出ます。透明な色を使っているときは、最悪の場合のコントラストが示されます。", "tooltipRadius": "ツールチップとアラート", @@ -381,9 +437,9 @@ "true": "はい" }, "notifications": "通知", - "notification_mutes": "特定のユーザーからの通知を止めるには、ミュートしてください。", - "notification_blocks": "ブロックしているユーザーからの通知は、すべて止まります。", - "enable_web_push_notifications": "ウェブプッシュ通知を許可する", + "notification_mutes": "特定のユーザーからの通知を止めるには、ミュートを使用してください。", + "notification_blocks": "ユーザーをブロックすると、そのユーザーからの通知はすべて停止されます。", + "enable_web_push_notifications": "プッシュ通知を有効にする", "style": { "switcher": { "keep_color": "色を残す", @@ -398,12 +454,12 @@ "help": { "snapshot_missing": "テーマのスナップショットがありません。思っていた見た目と違うかもしれません。", "migration_snapshot_ok": "念のために、テーマのスナップショットが読み込まれました。テーマのデータを読み込むことができます。", - "fe_downgraded": "フロントエンドが前のバージョンに戻りました。", - "fe_upgraded": "フロントエンドと一緒に、テーマエンジンが新しくなりました。", - "older_version_imported": "古いフロントエンドで作られたファイルをインポートしました。", - "future_version_imported": "新しいフロントエンドで作られたファイルをインポートしました。", - "v2_imported": "古いフロントエンドのためのファイルをインポートしました。設定した通りにならないかもしれません。", - "upgraded_from_v2": "フロントエンドが新しくなったので、今までの見た目と少し違うかもしれません。", + "fe_downgraded": "PleromaFEが前のバージョンに戻りました。", + "fe_upgraded": "PleromaFEのテーマエンジンが更新されました。", + "older_version_imported": "古いバージョンで作成されたファイルをインポートしました。", + "future_version_imported": "新しいバージョンで作成されたファイルをインポートしました。", + "v2_imported": "古いバージョンで作成されたファイルをインポートしました。設定した通りにならないかもしれません。", + "upgraded_from_v2": "PleromaFEが更新されました。テーマの表示が以前と異なる場合があります。", "snapshot_source_mismatch": "フロントエンドがロールバックと更新を繰り返したため、バージョンが競合しています。", "migration_napshot_gone": "スナップショットがありません、覚えているものと見た目が違うかもしれません。", "snapshot_present": "テーマのスナップショットが読み込まれました。設定は上書きされました。代わりとして実データを読み込むことができます。" @@ -432,7 +488,7 @@ "common_colors": { "_tab_label": "共通", "main": "共通の色", - "foreground_hint": "「詳細」タブで、もっと細かく設定できます", + "foreground_hint": "「詳細」タブで、より細かく設定できます", "rgbo": "アイコンとアクセントとバッジ" }, "advanced_colors": { @@ -445,7 +501,7 @@ "top_bar": "トップバー", "borders": "境界", "buttons": "ボタン", - "inputs": "インプットフィールド", + "inputs": "入力欄", "faint_text": "薄いテキスト", "alert_neutral": "それ以外", "chat": { @@ -483,7 +539,7 @@ "filter_hint": { "always_drop_shadow": "ブラウザーがサポートしていれば、常に {0} が使われます。", "drop_shadow_syntax": "{0} は、{1} パラメーターと {2} キーワードをサポートしていません。", - "avatar_inset": "内側の影と外側の影を同時に使うと、透明なアバターの表示が乱れます。", + "avatar_inset": "内側の影と外側の影を同時に使うと、透明なアイコンの表示がおかしくなることがあります。", "spread_zero": "広がりが 0 よりも大きな影は、0 と同じです", "inset_classic": "内側の影は {0} を使います" }, @@ -491,14 +547,14 @@ "panel": "パネル", "panelHeader": "パネルヘッダー", "topBar": "トップバー", - "avatar": "ユーザーアバター (プロフィール)", - "avatarStatus": "ユーザーアバター (投稿)", + "avatar": "ユーザーアイコン (プロフィール)", + "avatarStatus": "ユーザーアイコン (投稿)", "popup": "ポップアップとツールチップ", "button": "ボタン", "buttonHover": "ボタン (ホバー)", "buttonPressed": "ボタン (押されているとき)", "buttonPressedHover": "ボタン (ホバー、かつ、押されているとき)", - "input": "インプットフィールド" + "input": "入力欄" }, "hintV3": "影の場合は、 {0} 表記を使って他の色スロットを使うこともできます。" }, @@ -507,7 +563,7 @@ "help": "「カスタム」を選んだときは、システムにあるフォントの名前を、正しく入力してください。", "components": { "interface": "インターフェース", - "input": "インプットフィールド", + "input": "入力欄", "post": "投稿", "postCode": "等幅 (投稿がリッチテキストであるとき)" }, @@ -536,7 +592,7 @@ "backend_version": "バックエンドのバージョン", "frontend_version": "フロントエンドのバージョン" }, - "notification_setting_hide_notification_contents": "送った人と内容を、プッシュ通知に表示しない", + "notification_setting_hide_notification_contents": "送った人と通知の内容をプッシュ通知に表示しない", "notification_setting_privacy": "プライバシー", "notification_setting_block_from_strangers": "フォローしていないユーザーからの通知を拒否する", "notification_setting_filters": "フィルター", @@ -544,53 +600,55 @@ "virtual_scrolling": "タイムラインの描画を最適化する", "type_domains_to_mute": "ミュートしたいドメインを検索", "useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)", - "useStreamingApi": "投稿と通知を、すぐに受け取る", + "useStreamingApi": "投稿と通知をリアルタイムで受信する", "user_mutes": "ユーザー", - "reset_background_confirm": "本当にバックグラウンドを初期化しますか?", - "reset_banner_confirm": "本当にバナーを初期化しますか?", - "reset_avatar_confirm": "本当にアバターを初期化しますか?", - "hide_wallpaper": "インスタンスのバックグラウンドを隠す", - "reset_profile_background": "プロフィールのバックグラウンドを初期化", - "reset_profile_banner": "プロフィールのバナーを初期化", - "reset_avatar": "アバターを初期化", + "reset_background_confirm": "本当に背景をリセットしますか?", + "reset_banner_confirm": "本当にバナーをリセットしますか?", + "reset_avatar_confirm": "本当にアイコンをリセットしますか?", + "hide_wallpaper": "インスタンスデフォルトの壁紙を表示しない", + "reset_profile_background": "プロフィールの背景をリセット", + "reset_profile_banner": "プロフィールのバナーをリセット", + "reset_avatar": "アイコンをリセット", "notification_visibility_emoji_reactions": "リアクション", "notification_visibility_moves": "ユーザーの引っ越し", "new_email": "新しいメールアドレス", "profile_fields": { "value": "内容", "name": "ラベル", - "add_field": "枠を追加", - "label": "プロフィール補足情報" + "add_field": "入力欄を追加", + "label": "追加情報" }, "accent": "アクセント", - "mutes_imported": "ミュートをインポートしました!少し時間がかかるかもしれません。", - "emoji_reactions_on_timeline": "絵文字リアクションをタイムラインに表示", + "mutes_imported": "ミュートがインポートされました。処理には時間がかかる場合があります。", + "emoji_reactions_on_timeline": "絵文字リアクションをタイムラインに表示する", "domain_mutes": "ドメイン", "mutes_and_blocks": "ミュートとブロック", "chatMessageRadius": "チャットメッセージ", - "change_email_error": "メールアドレスを変えることが、できなかったかもしれません。", - "changed_email": "メールアドレスが、変わりました!", - "change_email": "メールアドレスを変える", + "change_email_error": "メールアドレスの変更中にエラーが発生しました。", + "changed_email": "メールアドレスが変更されました!", + "change_email": "メールアドレスを変更", "bot": "これは bot アカウントです", "mute_export_button": "ミュートをCSVファイルにエクスポートする", "import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする", "mute_import_error": "ミュートのインポートに失敗しました", "mute_import": "ミュートのインポート", "mute_export": "ミュートのエクスポート", - "allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する", - "setting_changed": "規定の設定と異なっています", - "greentext": "引用を緑色で表示", - "sensitive_by_default": "はじめから投稿をセンシティブとして設定", + "allow_following_move": "フォローしているアカウントが引っ越したとき、引っ越し先を自動でフォローする", + "setting_changed": "デフォルトから変更された設定", + "greentext": "Meme arrows", + "sensitive_by_default": "デフォルトで投稿を閲覧注意として設定", "more_settings": "その他の設定", - "reply_visibility_self_short": "自分宛のリプライを見る", - "reply_visibility_following_short": "フォローしている人に宛てられたリプライを見る", - "hide_all_muted_posts": "ミュートした投稿を隠す", - "hide_media_previews": "メディアのプレビューを隠す", + "reply_visibility_self_short": "自分宛ての返信のみ表示", + "reply_visibility_following_short": "フォローしているユーザー宛ての返信のみ表示", + "hide_all_muted_posts": "ミュートした投稿を表示しない", + "hide_media_previews": "メディアのプレビューを表示しない", "word_filter": "単語フィルタ", "file_export_import": { "errors": { - "invalid_file": "これはPleromaの設定をバックアップしたファイルではありません。", - "file_slightly_new": "ファイルのマイナーバージョンが異なり、一部の設定が読み込まれないことがあります" + "invalid_file": "非対応の形式の設定ファイルです。設定は変更されませんでした。", + "file_slightly_new": "設定ファイルのバージョンが異なります。一部の設定は読み込まれないかもしれません", + "file_too_new": "互換性エラー: PleromaFEが古すぎます。設定ファイルのバージョン{fileMajor}はこのPleromaFE (バージョン{feMajor})と互換性がありません", + "file_too_old": "互換性エラー: 設定ファイルが古すぎます。設定ファイルのバージョン{fileMajor}はこのPleromaFE (バージョン{feMajor})と互換性がありません" }, "restore_settings": "設定をファイルから復元する", "backup_settings_theme": "テーマを含む設定をファイルにバックアップする", @@ -599,8 +657,152 @@ }, "save": "変更を保存", "hide_shoutbox": "Shoutboxを表示しない", - "always_show_post_button": "投稿ボタンを常に表示", - "right_sidebar": "サイドバーを右に表示" + "always_show_post_button": "投稿ボタンを常に表示する", + "right_sidebar": "サイドバーを右側に表示する", + "email_language": "このサーバーから受け取るメールの言語", + "confirm_dialogs": "以下のとき確認ダイアログを表示する:", + "confirm_dialogs_repeat": "ステータスをリピートするとき", + "confirm_dialogs_unfollow": "ユーザーのフォローを解除するとき", + "confirm_dialogs_block": "ユーザーをブロックするとき", + "confirm_dialogs_mute": "ユーザーをミュートするとき", + "confirm_dialogs_delete": "投稿を削除するとき", + "confirm_dialogs_logout": "ログアウトするとき", + "confirm_dialogs_deny_follow": "フォローリクエストを却下するとき", + "confirm_dialogs_remove_follower": "フォロワーを解除するとき", + "move_account_target": "引っ越し先のアカウント (例: {example})", + "move_account_error": "引っ越し中にエラーが発生しました: {error}", + "autocomplete_select_first": "オートコンプリートで最初の結果を自動的に選択する", + "hide_bot_indication": "bot アカウントであることを示すマークを表示しない", + "navbar_column_stretch": "ナビゲーションバーの幅を画面幅に合わせる", + "notification_visibility_follow_requests": "フォローリクエスト", + "notification_visibility_reports": "通報", + "notification_extra_chats": "未読のチャットを表示する", + "hide_favorites_description": "お気に入り欄を非公開にする (通知は送信されます)", + "conversation_display_tree": "ツリー形式", + "max_depth_in_thread": "デフォルトで表示するスレッドの深さ", + "mention_link_display": "メンションリンクを", + "mention_link_display_short": "常に短く表示する (例: {'@'}hoge)", + "mention_link_use_tooltip": "メンションリンクをクリックした時ユーザーカードを表示する", + "mention_link_show_avatar": "メンションリンクの横にユーザーのアイコンを表示する", + "mention_link_display_full_for_remote": "リモートのユーザーのみすべて表示する (例: {'@'}hoge{'@'}example.org)", + "mention_link_display_full": "常にすべて表示する (例: {'@'}hoge{'@'}example.org)", + "notification_setting_filters_chrome_push": "Chromeなどのブラウザでは、種類に応じた通知の無効化がプッシュ通知に反映されない場合があります", + "hard_reset_value_tooltip": "データベースから設定値を削除し、デフォルト値に戻します", + "disable_sticky_headers": "カラムヘッダーを画面上部に固定しない", + "column_sizes_notifs": "通知カラム", + "conversation_other_replies_button": "「その他の返信」ボタンの位置", + "use_websockets": "Websocketを利用してリアルタイムで更新を行う", + "mention_link_fade_domain": "メンションのドメイン部分を薄く表示する (例: {'@'}foo{'@'}example.org の {'@'}example.org の部分)", + "mention_link_show_avatar_quick": "メンションの横にユーザーアイコンを表示する", + "mention_link_bolden_you": "自分宛てのメンションを強調表示する", + "user_popover_avatar_action": "ユーザーカード内のユーザーアイコンをクリックした際の挙動", + "user_popover_avatar_overlay": "ユーザーカードをユーザーアイコンに被せて表示する", + "show_yous": "自分宛てのメンションの横に「(あなた)」と表示する", + "preview": "プレビュー", + "url": "URL", + "conversation_display": "スレッドの表示形式", + "column_sizes": "カラム幅", + "third_column_mode_none": "表示しない", + "column_sizes_content": "コンテンツ", + "third_column_mode_notifications": "通知カラムにする", + "third_column_mode_postform": "投稿フォームとナビゲーションにする", + "conversation_display_linear_quick": "時系列表示", + "conversation_display_linear": "時系列形式", + "conversation_display_tree_quick": "ツリー表示", + "user_popover_avatar_action_open": "プロフィールを表示する", + "account_backup": "アカウントのバックアップ", + "wordfilter": "ワードフィルター", + "column_sizes_sidebar": "サイドバー", + "emoji_reactions_scale": "絵文字リアクションの表示倍率", + "hide_wordfiltered_statuses": "ワードフィルターによってフィルタリングされたステータスを表示しない", + "hide_muted_threads": "ミュートしたスレッドを表示しない", + "notification_visibility_polls": "投票結果の確定", + "user_popover_avatar_action_zoom": "アイコンを拡大する", + "post_look_feel": "投稿の表示形式", + "mention_links": "メンションのリンク", + "setting_server_side": "この設定はサーバー側に保存され、すべてのセッションとクライアントに影響します", + "word_filter_and_more": "ワードフィルターとその他の設定", + "notification_extra_announcements": "未読のお知らせを表示する", + "notification_extra_follow_requests": "新着のフォローリクエストを表示する", + "show_scrollbars": "サイドカラムにスクロールバーを表示する", + "third_column_mode": "十分に幅があるとき、三つ目のカラムを", + "columns": "カラム", + "commit_value": "保存", + "commit_value_tooltip": "値は保存されていません。反映するにはこのボタンを押してください", + "remove_backup": "削除", + "add_backup": "新規バックアップを作成", + "account_backup_description": "アカウント情報と投稿のアーカイブをダウンロードできます。開発段階の機能であり、現状、ダウンロードしたデータをインポートすることはできません。", + "mute_bot_posts": "BOTアカウントの投稿をミュートする", + "auto_update": "自動でタイムラインを更新する", + "enable_web_push_always_show_tip": "この設定は、Chromeなどのブラウザで「このサイトはバックグラウンドで更新されました」という通知が表示されることを防止します。その他のブラウザでこの設定を有効化すると、通知が二重で表示されることがあります。", + "backup_failed": "バックアップに失敗しました。", + "confirm_dialogs_approve_follow": "フォローリクエストを承認するとき", + "moved_account": "アカウントの引っ越しが完了しました。", + "reset_value": "リセット", + "reset_value_tooltip": "編集中の値を破棄します", + "hard_reset_value": "デフォルトに戻す", + "conversation_other_replies_button_below": "投稿の下", + "conversation_other_replies_button_inside": "投稿の中", + "add_language": "代替言語を追加", + "remove_language": "削除", + "account_alias_table_head": "エイリアス", + "account_alias": "アカウントエイリアス", + "list_aliases_error": "エイリアスの取得中にエラーが発生しました: {error}", + "hide_list_aliases_error_action": "閉じる", + "remove_alias": "削除", + "new_alias_target": "エイリアスを追加 (例: {example})", + "added_alias": "エイリアスが追加されました。", + "add_alias_error": "エイリアスの追加中にエラーが発生しました: {error}", + "move_account": "アカウントの引っ越し", + "move_account_notes": "アカウントを引っ越すためには、まず引っ越し先のアカウントにこのアカウントへのエイリアスを追加する必要があります。", + "birthday": { + "label": "誕生日", + "show_birthday": "誕生日を公開する" + }, + "account_privacy": "プライバシー", + "posts": "投稿", + "user_profiles": "ユーザープロフィール", + "primary_language": "第一言語:", + "fallback_language": "代替言語 {index}:", + "expert_mode": "高度な設定を表示", + "account_backup_table_head": "バックアップ", + "download_backup": "ダウンロード", + "backup_not_ready": "まだ準備中です。", + "backup_running": "処理中…{number}件のデータが処理されました。 | 処理中… {number}件のデータが処理されました。", + "list_backups_error": "バックアップ一覧の取得に失敗しました: {error}", + "added_backup": "バックアップがキューに追加されました。", + "add_backup_error": "バックアップの追加に失敗しました: {error}", + "user_popover_avatar_action_close": "ユーザーカードを閉じる", + "tree_advanced": "高度なナビゲーションボタンを表示する", + "tree_fade_ancestors": "スレッド上で祖先にあたるステータスを薄いテキストで表示する", + "actor_type_description": "グループとして設定されたアカウントは、メンションのついたステータスを自動的にリピートします。", + "actor_type_Person": "通常アカウント", + "actor_type_Service": "BOTアカウント", + "actor_type_Group": "グループアカウント", + "notification_visibility_in_column": "通知カラム(PC)、通知サイドバー(モバイル)に表示する", + "notification_setting_annoyance": "通知のカスタマイズ", + "notification_setting_unseen_at_top": "未読の通知を最上部に表示する", + "enable_web_push_always_show": "プッシュ通知を常に表示する", + "hide_scrobbles": "Scrobbleを表示しない", + "actor_type": "アカウントタイプ:", + "hide_actor_type_indication": "投稿にアカウントタイプ(BOTアカウント、グループアカウントなど)を示すアイコンを表示しない", + "notification_show_extra": "その他の通知を通知カラムに表示する", + "notification_setting_drawer_marks_as_seen": "モバイルUIで、通知サイドバーを閉じた時すべての通知を既読にする", + "notification_setting_ignore_inactionable_seen_tip": "この設定は通知を自動的に既読にするわけではなく、この設定を有効にしてもプッシュ通知などは届きます", + "notification_setting_ignore_inactionable_seen": "お気に入りやリピートの通知など、アクション不可な通知を未読として扱わない", + "notification_extra_tip": "通知カラムをカスマイズするためのヒントを表示する", + "use_at_icon": "メンションリンク内の{'@'}記号を画像にする", + "mute_sensitive_posts": "閲覧注意な投稿をミュートする", + "units": { + "time": { + "m": "分", + "s": "秒", + "h": "時間", + "d": "日" + } + }, + "hide_scrobbles_after": "これより古いScrobbleを表示しない:", + "force_theme_recompilation_debug": "テーマのキャッシュを無効化し、起動の度にコンパイルし直す (デバッグ用)" }, "time": { "day": "{0}日", @@ -622,7 +824,7 @@ "month_short": "{0}ヶ月前", "months_short": "{0}ヶ月前", "now": "たった今", - "now_short": "たった今", + "now_short": "今", "second": "{0}秒", "seconds": "{0}秒", "second_short": "{0}秒", @@ -634,23 +836,41 @@ "year": "{0}年", "years": "{0}年", "year_short": "{0}年", - "years_short": "{0}年" + "years_short": "{0}年", + "unit": { + "seconds_short": "{0}秒", + "weeks": "{0} 週間 | {0} 週間", + "weeks_short": "{0}週", + "years": "{0} 年 | {0} 年", + "years_short": "{0}年", + "days": "{0} 日 | {0} 日", + "hours": "{0} 時間 | {0} 時間", + "hours_short": "{0}時間", + "minutes": "{0} 分 | {0} 分", + "minutes_short": "{0}分", + "months": "{0} ヶ月 | {0} ヶ月", + "months_short": "{0}ヶ月", + "seconds": "{0} 秒 | {0} 秒", + "days_short": "{0}日" + } }, "timeline": { "collapse": "たたむ", "conversation": "スレッド", "error_fetching": "読み込みがエラーになりました", - "load_older": "古いステータス", - "no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります", + "load_older": "古いステータスを読み込む", + "no_retweet_hint": "公開範囲が「フォロワーのみ」または「ダイレクト」の投稿はリピートできません", "repeated": "リピート", "show_new": "読み込み", "up_to_date": "最新", "no_more_statuses": "これで終わりです", "no_statuses": "ステータスはありません", "reload": "再読み込み", - "error": "タイムラインの読み込みに失敗しました: {0}", + "error": "タイムラインの読み込み中にエラーが発生しました: {0}", "socket_reconnected": "リアルタイム接続が確立されました", - "socket_broke": "コード{0}によりリアルタイム接続が切断されました" + "socket_broke": "リアルタイム接続が切断されました: コード{0}", + "quick_view_settings": "表示の簡易設定", + "quick_filter_settings": "フィルターの簡易設定" }, "status": { "favorites": "お気に入り", @@ -659,8 +879,8 @@ "pin": "プロフィールにピン留め", "unpin": "プロフィールのピン留めを外す", "pinned": "ピン留め", - "delete_confirm": "本当にこのステータスを削除してもよろしいですか?", - "reply_to": "返信", + "delete_confirm": "本当に削除しますか?", + "reply_to": "返信先:", "replies_list": "返信:", "mute_conversation": "スレッドをミュート", "unmute_conversation": "スレッドのミュートを解除", @@ -679,19 +899,60 @@ "unbookmark": "ブックマーク解除", "bookmark": "ブックマーク", "mentions": "メンション", - "you": "(あなた)", - "plus_more": "ほか{number}件" + "you": "(あなた)", + "plus_more": "ほか{number}件", + "delete_confirm_title": "削除の確認", + "ancestor_follow": "このステータスについた{numReplies}件の返信をすべて表示 | このステータスについた{numReplies}件の返信をすべて表示", + "invisible_quote": "引用先のステータスが存在しません: {link}", + "ancestor_follow_with_icon": "{icon} {text}", + "show_all_conversation_with_icon": "{icon} {text}", + "delete_error": "ステータスの削除中にエラーが発生しました: {0}", + "delete_confirm_accept_button": "削除する", + "delete_confirm_cancel_button": "削除しない", + "collapse_attachments": "ファイルをたたむ", + "show_all_attachments": "すべてのファイルを表示", + "hide_attachment": "ファイルを隠す", + "reaction_count_label": "{num}人がリアクションしました | {num}人がリアクションしました", + "repeat_confirm_accept_button": "リピートする", + "repeat_confirm_cancel_button": "リピートしない", + "repeat_confirm": "本当にリピートしますか?", + "edit": "ステータスを編集", + "edited_at": "(最終編集: {time})", + "repeat_confirm_title": "リピートの確認", + "many_attachments": "この投稿には{number}件のファイルが添付されています", + "remove_attachment": "ファイルを削除", + "attachment_stop_flash": "Flashプレーヤーを停止", + "move_up": "ファイルを左へ移動", + "move_down": "ファイルを右へ移動", + "thread_follow": "このスレッドの残りを表示 (全部で{numStatus}件の投稿があります) | このスレッドの残りを表示 (全部で{numStatus}件の投稿があります)", + "thread_follow_with_icon": "{icon} {text}", + "hide_quote": "引用先を隠す", + "display_quote": "引用先を表示", + "show_only_conversation_under_this": "このステータスへの返信のみを表示", + "show_all_conversation": "スレッドの全体を表示 ({numStatus}件のステータス) | スレッドの全体を表示 ({numStatus}件のステータス)", + "replies_list_with_others": "返信 (+{numReplies}人): | 返信 (+{numReplies}人):", + "more_actions": "その他のアクション", + "thread_show_full": "このスレッドをすべて表示 (全部で{depth}層、{numStatus}件の投稿があります) | このスレッドを全て表示 (全部で{depth}層、{numStatus}件の投稿があります)", + "thread_show_full_with_icon": "{icon} {text}", + "show_attachment_in_modal": "メディアビューアで開く", + "show_attachment_description": "メディアの説明文をポップアップで表示 (全文を読むにはメディアを開いてください)", + "thread_hide": "このスレッドをたたむ", + "thread_show": "このスレッドを開く", + "open_gallery": "メディアビューアで開く", + "status_history": "編集履歴", + "sensitive_muted": "閲覧注意な投稿のためミュートされています", + "load_error": "投稿の読み込みに失敗しました: {error}" }, "user_card": { - "approve": "受け入れ", + "approve": "承認", "block": "ブロック", "blocked": "ブロックしています!", - "deny": "お断り", + "deny": "拒否", "favorites": "お気に入り", "follow": "フォロー", - "follow_sent": "リクエストを送りました!", + "follow_sent": "リクエストを送信しました!", "follow_progress": "リクエストしています…", - "follow_unfollow": "フォローをやめる", + "follow_unfollow": "フォロー解除", "followees": "フォロー", "followers": "フォロワー", "following": "フォローしています!", @@ -700,7 +961,7 @@ "media": "メディア", "mention": "メンション", "mute": "ミュート", - "muted": "ミュートしています", + "muted": "ミュート済み", "per_day": "/日", "remote_follow": "リモートフォロー", "report": "通報", @@ -720,16 +981,17 @@ "grant_moderator": "モデレーター権限を付与", "revoke_moderator": "モデレーター権限を解除", "activate_account": "アカウントをアクティブにする", - "deactivate_account": "アカウントをアクティブでなくする", + "deactivate_account": "アカウントを無効化する", "delete_account": "アカウントを削除", - "force_nsfw": "すべての投稿をNSFWにする", - "strip_media": "投稿からメディアを除去する", - "force_unlisted": "投稿を未収載にする", - "sandbox": "投稿をフォロワーのみにする", - "disable_remote_subscription": "他のインスタンスからフォローされないようにする", - "disable_any_subscription": "フォローされないようにする", - "quarantine": "他のインスタンスからの投稿を止める", - "delete_user": "ユーザーを削除" + "force_nsfw": "すべての投稿を閲覧注意にする", + "strip_media": "すべての投稿からメディアを除去する", + "force_unlisted": "すべての投稿をアンリステッドにする", + "sandbox": "すべての投稿をフォロワー限定にする", + "disable_remote_subscription": "他のインスタンスからフォローできないようにする", + "disable_any_subscription": "フォローできないようにする", + "quarantine": "投稿を連合しないようにする", + "delete_user": "ユーザーを削除", + "delete_user_data_and_deactivate_confirmation": "このアカウントのデータを永久に削除し、アカウントを無効化します。本当によろしいですね?" }, "roles": { "moderator": "モデレーター", @@ -738,7 +1000,7 @@ "show_repeats": "リピートを見る", "hide_repeats": "リピートを隠す", "message": "メッセージ", - "hidden": "隠す", + "hidden": "非公開", "bot": "bot", "highlight": { "solid": "背景を単色にする", @@ -746,21 +1008,55 @@ "side": "端に線を付ける", "disabled": "強調しない" }, - "edit_profile": "プロフィールを編集" + "edit_profile": "プロフィールを編集", + "deny_confirm_accept_button": "拒否する", + "note_blank": "(なし)", + "edit_note_cancel": "キャンセル", + "remove_follower_confirm_cancel_button": "解除しない", + "block_confirm_title": "ブロックの確認", + "block_confirm": "本当に{user}をブロックしますか?", + "birthday": "誕生日: {birthday}", + "edit_note": "メモを編集", + "edit_note_apply": "適用", + "note": "メモ", + "remove_follower_confirm": "本当に{user}からのフォローを解除しますか?", + "follow_cancel": "リクエストを取り消す", + "approve_confirm_title": "承認の確認", + "remove_follower": "フォロワーを解除", + "remove_follower_confirm_title": "フォロワー解除の確認", + "approve_confirm_accept_button": "承認する", + "deny_confirm": "本当に{user}からのフォローリクエストを拒否しますか?", + "deny_confirm_cancel_button": "拒否しない", + "mute_confirm_cancel_button": "ミュートしない", + "approve_confirm_cancel_button": "承認しない", + "approve_confirm": "本当に{user}からのフォローリクエストを承認しますか?", + "unfollow_confirm_title": "フォロー解除の確認", + "unfollow_confirm_cancel_button": "解除しない", + "mute_confirm_accept_button": "ミュートする", + "mute_confirm": "本当に{user}をミュートしますか?", + "block_confirm_accept_button": "ブロックする", + "block_confirm_cancel_button": "ブロックしない", + "deny_confirm_title": "拒否の確認", + "unfollow_confirm": "本当に{user}のフォローを解除しますか?", + "unfollow_confirm_accept_button": "解除する", + "remove_follower_confirm_accept_button": "解除する", + "mute_confirm_title": "ミュートの確認", + "deactivated": "無効化済み", + "group": "グループ" }, "user_profile": { "timeline_title": "ユーザータイムライン", - "profile_does_not_exist": "申し訳ない。このプロフィールは存在しません。", - "profile_loading_error": "申し訳ない。プロフィールの読み込みがエラーになりました。" + "profile_does_not_exist": "このプロフィールは存在しません。", + "profile_loading_error": "プロフィールの読み込み中にエラーが発生しました。" }, "user_reporting": { - "title": "通報する: {0}", + "title": "{0}を通報する", "add_comment_description": "この通報は、あなたのインスタンスのモデレーターに送られます。このアカウントを通報する理由を説明することができます:", "additional_comments": "追加のコメント", - "forward_description": "このアカウントは他のサーバーに置かれています。この通報のコピーをリモートのサーバーに送りますか?", - "forward_to": "転送する: {0}", + "forward_description": "これは他のインスタンスのアカウントです。この通報のコピーをリモートのインスタンスに送りますか?", + "forward_to": "{0}に転送する", "submit": "送信", - "generic_error": "あなたのリクエストを処理しようとしましたが、エラーになりました。" + "generic_error": "リクエストの処理中にエラーが発生しました。" }, "who_to_follow": { "more": "詳細", @@ -774,15 +1070,17 @@ "user_settings": "ユーザー設定", "bookmark": "ブックマーク", "reject_follow_request": "フォローリクエストを拒否", - "accept_follow_request": "フォローリクエストを許可", - "add_reaction": "リアクションを追加" + "accept_follow_request": "フォローリクエストを承認", + "add_reaction": "リアクションを追加", + "toggle_mute": "ミュートされた通知を開く/閉じる", + "toggle_expand": "この投稿を開く/閉じる" }, "upload": { "error": { "base": "アップロードに失敗しました。", - "file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", - "default": "しばらくしてから試してください", - "message": "アップロードに失敗: {0}" + "file_too_big": "ファイルが大きすぎます [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "時間を置いて再試行してください", + "message": "アップロードに失敗しました: {0}" }, "file_size_units": { "B": "B", @@ -793,11 +1091,13 @@ } }, "search": { - "people": "人々", + "people": "ユーザー", "hashtags": "ハッシュタグ", "person_talking": "{count} 人が話しています", "people_talking": "{count} 人が話しています", - "no_results": "見つかりませんでした" + "no_results": "見つかりませんでした", + "load_more": "さらに読み込む", + "no_more_results": "結果は以上です" }, "password_reset": { "forgot_password": "パスワードを忘れましたか?", @@ -817,23 +1117,25 @@ "federation": "連合", "simple": { "media_nsfw_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを閲覧注意に設定します:", - "media_nsfw": "メディアを閲覧注意に設定", + "media_nsfw": "強制閲覧注意", "media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:", "media_removal": "メディア除去", - "ftl_removal": "「既知のネットワーク」タイムラインから除外", - "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「既知のネットワーク」タイムラインから除外します:", + "ftl_removal": "「すべてのネットワーク」タイムラインから除外", + "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「すべてのネットワーク」タイムラインから除外します:", "quarantine_desc": "このインスタンスでは、以下のインスタンスに対して公開投稿のみを送信します:", "quarantine": "検疫", "reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:", "accept_desc": "このインスタンスでは、以下のインスタンスからのメッセージのみを受け付けます:", "accept": "許可", - "simple_policies": "インスタンス固有のポリシー", - "reject": "拒否" + "simple_policies": "インスタンスに対するポリシー", + "reject": "拒否", + "instance": "インスタンス", + "reason": "理由" }, "mrf_policies": "有効なMRFポリシー", "keyword": { - "replace": "置き換え", - "ftl_removal": "「接続しているすべてのネットワーク」タイムラインから除外", + "replace": "置換", + "ftl_removal": "「すべてのネットワーク」タイムラインから除外", "keyword_policies": "キーワードポリシー", "is_replaced_by": "→", "reject": "拒否" @@ -847,8 +1149,8 @@ "file_type": { "file": "ファイル", "image": "画像", - "video": "ビデオ", - "audio": "オーディオ" + "video": "動画", + "audio": "音声" }, "remote_user_resolver": { "error": "見つかりませんでした。", @@ -865,7 +1167,7 @@ "empty_chat_list_placeholder": "チャットはありません。新規チャットのボタンを押して始めましょう!", "error_sending_message": "メッセージの送信に失敗しました。", "error_loading_chat": "チャットの読み込みに失敗しました。", - "delete_confirm": "このメッセージを本当に消してもいいですか?", + "delete_confirm": "本当にこのメッセージを削除しますか?", "more": "もっと見る", "empty_message_error": "メッセージを入力して下さい", "new": "新規チャット", @@ -879,5 +1181,191 @@ "unmute": "ミュート解除", "mute_progress": "ミュート中…", "mute": "ミュート" + }, + "admin_dash": { + "window_title": "管理者設定", + "nodb": { + "text": "{property}が{value}に設定されるよう、設定ファイルを編集する必要があります。詳しくは{documentation}を確認してください。", + "documentation": "ドキュメント", + "text2": "ほとんどの設定項目は利用できません。", + "heading": "データベースへの設定の保存は無効化されています" + }, + "captcha": { + "native": "ネイティブ", + "kocaptcha": "KoCaptcha" + }, + "instance": { + "restrict": { + "header": "匿名ユーザーへのアクセス制限", + "profiles": "ユーザープロフィールへのアクセス", + "timelines": "タイムラインへのアクセス", + "activities": "ステータスへのアクセス", + "description": "この設定は特定のAPIへのアクセスを制御します。デフォルトでは、インスタンスの公開設定を反映します。一部の設定は、変更すると予期しない動作を引き起こすことがあります(例: ユーザープロフィールへのアクセスを禁止すると、投稿にユーザーの情報が表示されなくなります)。" + }, + "instance": "インスタンス情報", + "registrations": "ユーザー登録", + "access": "インスタンスへのアクセス", + "captcha_header": "CAPTCHA", + "kocaptcha": "KoCaptchaの設定" + }, + "frontend": { + "available_frontends": "インストール可能なフロントエンド", + "success_installing_frontend": "{version} は正常にインストールされました", + "failure_installing_frontend": "{version} のインストールに失敗しました: {reason}", + "repository": "リポジトリのURL", + "versions": "利用可能なバージョン", + "build_url": "ダウンロードURL", + "reinstall": "再インストール", + "install": "インストール", + "install_version": "バージョン {version} をインストール", + "is_default": "(デフォルト)", + "is_default_custom": "(デフォルト、バージョン: {version})", + "default_frontend": "デフォルトのフロントエンド", + "more_install_options": "その他のインストールオプション", + "wip_notice": "このセクションは開発段階です。バックエンド側の実装が未完成であるため、一部の機能は欠けています。", + "set_default": "デフォルトに設定", + "set_default_version": "バージョン {version} をデフォルトに設定", + "default_frontend_tip": "デフォルトのフロントエンドはすべてのユーザーに表示されます。現時点で、ユーザーがフロントエンドを選択する方法はありません。デフォルトのフロントエンドをPleromaFE以外に設定した場合、インスタンスの設定を変更するには古いAdminFEを使用する必要があります。" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":public": { + "label": "インスタンスを公開する", + "description": "この設定を無効化すると、すべてのAPIの使用にログインが必要になります。これにより、匿名ユーザーは公開タイムラインとすべてのネットワークにアクセスできなくなります。" + }, + ":background_image": { + "description": "(主にPleromaFEで使用される)背景画像", + "label": "背景画像" + }, + ":limit_to_local_content": { + "description": "他インスタンスの情報の検索を、未ログインのユーザー(デフォルト)もしくはすべてのユーザーに対して制限します", + "label": "検索をローカルのみに制限する" + }, + ":description_limit": { + "description": "ファイルの説明欄に対する文字数制限", + "label": "制限" + } + } + } + }, + "wip_notice": "この管理者用設定画面は試験段階であり、未完成です。{adminFeLink}。", + "reset_all": "すべてリセット", + "commit_all": "すべて保存", + "old_ui_link": "これまでの管理者画面にはここからアクセスできます", + "tabs": { + "limits": "制限", + "instance": "インスタンス", + "frontends": "フロントエンド", + "emoji": "絵文字" + }, + "limits": { + "arbitrary_limits": "任意の制限", + "posts": "投稿の制限", + "uploads": "ファイルの制限", + "profile_fields": "追加情報欄の制限", + "user_uploads": "プロフィール画像の制限", + "users": "ユーザープロフィールの設定" + }, + "emoji": { + "create_pack": "パックを作成", + "delete_pack": "パックを削除", + "create": "作成", + "emoji_packs": "絵文字パック", + "remote_packs": "リモートのパック", + "emoji_pack": "絵文字パック", + "edit_pack": "パックを編集", + "homepage": "ホームページ", + "save": "保存", + "save_meta": "メタデータを保存", + "shortcode": "ショートコード", + "filename": "ファイル名", + "delete_confirm": "{0}を削除してもよろしいですか?", + "download_pack": "パックをダウンロード", + "downloading_pack": "{0}をダウンロード中", + "download": "ダウンロード", + "editing": "{0}を編集中", + "error": "エラー: {0}", + "delete": "削除", + "global_actions": "グローバルアクション", + "reload": "絵文字を再読み込み", + "new_pack_name": "新規パック名", + "fallback_sha256": "代替ソースのSHA256ハッシュ", + "description": "説明", + "fallback_src": "代替ソース", + "share": "共有", + "add_file": "ファイルを追加", + "adding_new": "新規絵文字を追加", + "revert_meta": "メタデータを元に戻す", + "revert": "元に戻す", + "new_shortcode": "ショートコード (任意)", + "new_filename": "ファイル名 (任意)", + "files": "ファイル", + "delete_title": "削除しますか?", + "metadata_changed": "変更されたメタデータ", + "emoji_changed": "保存されていない変更点があります。ハイライトされた絵文字を確認してください" + } + }, + "lists": { + "search": "ユーザーを検索", + "update_title": "リスト名を保存", + "really_delete": "本当に削除しますか?", + "error": "リストの処理中にエラーが発生しました: {0}", + "lists": "リスト", + "new": "新規リスト", + "save": "変更を保存", + "delete": "リストを削除", + "editing_list": "{listTitle}の編集", + "creating_list": "新規リストの作成", + "create": "作成", + "title": "リスト名", + "following_only": "フォローしているユーザーのみ表示", + "manage_lists": "リストの管理", + "manage_members": "メンバーの管理", + "add_members": "メンバーの追加", + "remove_from_list": "リストから削除", + "add_to_list": "リストに追加", + "is_in_list": "追加済み" + }, + "update": { + "update_bugs": "何か問題を見つけたら{pleromaGitlab}にて報告してください。開発中のバージョンにて念入りに確認はしましたが、様々なものが変更されているため、我々が見逃したものがあるかもしれません。バグの報告や、Pleroma/PleromaFEを改善するための提案やフィードバックは大歓迎です。", + "update_changelog_here": "変更履歴", + "update_changelog": "全ての変更点は{theFullChangelog}を参照してください。", + "big_update_content": "久しぶりのリリースですので、今までと異なるところがあるかもしれません。", + "update_bugs_gitlab": "Pleroma GitLab", + "big_update_title": "" + }, + "report": { + "reported_statuses": "通報されたステータス:", + "notes": "メモ:", + "state": "状態:", + "state_open": "未解決", + "reporter": "通報者:", + "state_resolved": "解決済み", + "reported_user": "被通報者:", + "state_closed": "問題なし" + }, + "unicode_domain_indicator": { + "tooltip": "このドメインには非ASCII文字が含まれています。" + }, + "announcements": { + "page_header": "お知らせ", + "title": "お知らせ", + "mark_as_read_action": "既読にする", + "post_form_header": "お知らせを投稿", + "post_placeholder": "お知らせの内容を入力してください…", + "post_action": "投稿", + "post_error": "エラー: {error}", + "close_error": "閉じる", + "delete_action": "削除", + "submit_edit_action": "完了", + "cancel_edit_action": "キャンセル", + "published_time_display": "{time} に公開", + "start_time_display": "{time}から開始", + "end_time_display": "{time}に終了", + "edit_action": "編集", + "start_time_prompt": "開始日時: ", + "end_time_prompt": "終了日時: ", + "all_day_prompt": "終日" } } diff --git a/src/i18n/ko.json b/src/i18n/ko.json @@ -109,7 +109,8 @@ "mobile_notifications_close": "알림 닫기", "mobile_sidebar": "모바일 사이드바 토글", "announcements": "공지사항", - "search_close": "검색 바 닫기" + "search_close": "검색 바 닫기", + "mobile_notifications_mark_as_seen": "모두 읽음으로 표시" }, "notifications": { "broken_favorite": "알 수 없는 게시물입니다, 검색합니다…", @@ -125,7 +126,13 @@ "error": "알림 불러오기 실패: {0}", "follow_request": "팔로우 요청", "submitted_report": "신고 내용을 전송함", - "poll_ended": "투표가 끝남" + "poll_ended": "투표가 끝남", + "unread_follow_requests": "{num}개의 새 팔로우 요청 | {num}개의 새 팔로우 요청", + "configuration_tip": "{theSettings}에서 어떻게 보이는지 바꿀 수 있습니다. {dismiss}", + "configuration_tip_settings": "설정", + "configuration_tip_dismiss": "다시 보지 않기", + "unread_announcements": "{num}개의 읽지 않은 공지사항 | {num}개의 읽지 않은 공지사항", + "unread_chats": "{num}개의 읽지 않은 채팅 | {num}개의 읽지 않은 채팅" }, "post_status": { "new_status": "새 게시물 게시", @@ -165,7 +172,9 @@ "post": "게시", "direct_warning_to_first_only": "맨 앞에 멘션한 사용자들에게만 보여집니다.", "content_type_selection": "게시물 형태", - "scope_notice_dismiss": "알림 닫기" + "scope_notice_dismiss": "알림 닫기", + "reply_option": "이 게시물에 답글", + "quote_option": "이 게시물을 인용" }, "registration": { "bio": "소개", @@ -558,7 +567,7 @@ "discoverable": "검색 결과나 다른 서비스들에서 이 계정을 찾을 수 있도록 허용", "pad_emoji": "에모지를 선택창에서 고를 때 띄어쓰기를 집어넣기", "wordfilter": "단어 필터", - "word_filter_and_more": "단어 필터 그리고 더보기...", + "word_filter_and_more": "단어 필터 및 기타 설정...", "accent": "강조", "hide_media_previews": "미디어 미리보기 숨기기", "max_thumbnails": "게시물 하나 당 최대로 보여질 섬네일 개수 (비워두면 제한을 두지 않습니다)", @@ -686,7 +695,15 @@ "remove_language": "삭제", "primary_language": "주 언어:", "fallback_language": "보조 언어 {index}:", - "confirm_dialogs_logout": "로그아웃" + "confirm_dialogs_logout": "로그아웃", + "url": "URL", + "preview": "미리보기", + "commit_value": "저장", + "commit_value_tooltip": "값이 저장되지 않았습니다, 버튼을 눌러 변경사항을 반영하세요", + "reset_value": "초기화", + "reset_value_tooltip": "변경사항 초기화", + "hard_reset_value": "완전 초기화", + "hard_reset_value_tooltip": "스토리지에서 설정을 제거하고, 기본값을 사용하도록 강제합니다" }, "timeline": { "collapse": "접기", @@ -703,7 +720,8 @@ "no_more_statuses": "새 게시물 없음", "socket_reconnected": "실시간 연결 됨", "socket_broke": "실시간 연결이 끊어짐: CloseEvent 코드 {0}", - "quick_filter_settings": "빠른 필터 설정" + "quick_filter_settings": "빠른 필터 설정", + "quick_view_settings": "빠른 뷰 설정" }, "user_card": { "approve": "승인", @@ -774,7 +792,33 @@ "approve_confirm_accept_button": "승인", "approve_confirm_cancel_button": "승인 안 함", "approve_confirm": "{user}의 팔로우 요청을 승인할까요?", - "block_confirm_title": "차단 확인" + "block_confirm_title": "차단 확인", + "note": "노트", + "unfollow_confirm": "정말 {user}를 팔로우 해제하시겠습니까?", + "unfollow_confirm_accept_button": "팔로우 해제", + "unfollow_confirm_cancel_button": "취소", + "remove_follower_confirm_title": "팔로워 삭제 확인", + "remove_follower_confirm_cancel_button": "냅두기", + "remove_follower_confirm_accept_button": "치우기", + "edit_note_cancel": "취소", + "birthday": "{birthday}에 태어남", + "edit_note": "노트 수정", + "edit_note_apply": "적용", + "deny_confirm_cancel_button": "취소", + "unfollow_confirm_title": "팔로우 해제 확인", + "mute_confirm_accept_button": "뮤트", + "remove_follower_confirm": "정말 {user}를 팔로워에서 치울까요?", + "deny_confirm_accept_button": "거절", + "mute_confirm_title": "뮤트 확인", + "mute_confirm": "정말 {user}를 뮤트할까요?", + "block_confirm_cancel_button": "취소", + "deny_confirm_title": "거절 확인", + "block_confirm": "정말 {user}를 차단할까요?", + "block_confirm_accept_button": "차단", + "mute_confirm_cancel_button": "취소", + "mute_duration_prompt": "이 사용자를 뮤트할 시간 (0으로 두면 무한히):", + "deny_confirm": "{user}의 팔로 요청을 거절할까요?", + "note_blank": "(없음)" }, "user_profile": { "timeline_title": "사용자 타임라인", @@ -794,7 +838,10 @@ "add_reaction": "반응 추가", "accept_follow_request": "팔로우 요청 승인", "reject_follow_request": "팔로우 요청 거절", - "bookmark": "북마크" + "bookmark": "북마크", + "autocomplete_available": "{number}개의 결과가 있습니다. 위 또는 아래 화살표 키로 탐색할 수 있습니다. | {number}개의 결과가 있습니다. 위 또는 아래 화살표 키로 탐색할 수 있습니다.", + "toggle_expand": "알림을 펼치거나 접어서 전체 게시물을 보기", + "toggle_mute": "알림을 펼치거나 접어서 뮤트한 내용 보기" }, "upload": { "error": { @@ -840,7 +887,8 @@ "symbols": "기호" }, "keep_open": "열린 채로 두기", - "regional_indicator": "지역 표시기 {letter}" + "regional_indicator": "지역 표시기 {letter}", + "unpacked": "미분류 에모지" }, "polls": { "add_poll": "투표를 추가", @@ -1049,7 +1097,8 @@ "update_changelog_here": "변경 내역", "update_changelog": "무엇이 바뀌었는지 자세히 알아보시려면, {theFullChangelog}을 참조하세요.", "big_update_content": "저희가 한동안 릴리즈를 안 해서, 익숙하셨던 생김새나 경험과 많이 달라졌을 수 있습니다.", - "update_bugs": "저희가 비록 테스트를 많이 하고 직접 개발 버전을 쓰기도 하지만, 많이 바꾸기도 했고, 몇몇 가지 놓친 점들이 있을 터이니, 사용하면서 불편한 점이나 문제는 {pleromaGitlab}에 제보해주시면 감사하겠습니다. 저희는 겪으신 문제점이나 Pleroma와 Pleroma-FE에 대한 피드백과 제안을 환영합니다." + "update_bugs": "저희가 비록 테스트를 많이 하고 직접 개발 버전을 쓰기도 하지만, 많이 바꾸기도 했고, 몇몇 가지 놓친 점들이 있을 터이니, 사용하면서 불편한 점이나 문제는 {pleromaGitlab}에 제보해주시면 감사하겠습니다. 저희는 겪으신 문제점이나 Pleroma와 Pleroma-FE에 대한 피드백과 제안을 환영합니다.", + "art_by": "{linkToArtist} 그림" }, "unicode_domain_indicator": { "tooltip": "이 도메인은 아스키 문자가 아닌 문자를 포함하고 있습니다." @@ -1115,7 +1164,9 @@ "repeat_confirm_cancel_button": "리핏 안 함", "delete_confirm_title": "삭제 확인", "delete_confirm_accept_button": "삭제", - "delete_confirm_cancel_button": "냅두기" + "delete_confirm_cancel_button": "냅두기", + "delete_error": "게시물 삭제 에러: {0}", + "reaction_count_label": "{num}명이 반응함 | {num}명이 반응함" }, "errors": { "storage_unavailable": "Pleroma가 브라우저 저장소에 접근할 수 없습니다. 로그인이 풀리거나 로컬 설정이 초기화 되는 등 예상치 못한 문제를 겪을 수 있습니다. 쿠키를 활성화 해보세요." @@ -1159,5 +1210,91 @@ "submit_edit_action": "수정본 반영", "cancel_edit_action": "취소", "inactive_message": "이 공지사항은 비활성화 되었습니다" + }, + "admin_dash": { + "window_title": "관리", + "wip_notice": "이 관리자 대시보드는 실험적이며 개발 중에 있습니다, {adminFeLink}.", + "old_ui_link": "대신 구 관리자 UI를 사용할 수 있습니다", + "reset_all": "전부 초기화", + "commit_all": "전부 저장", + "tabs": { + "nodb": "DB 설정 불가", + "instance": "인스턴스", + "frontends": "프론트엔드", + "limits": "제한" + }, + "nodb": { + "heading": "데이터베이스 설정 기능이 비활성화 되어 있습니다", + "documentation": "관련 문서", + "text2": "대부분의 설정을 건드릴 수 없습니다.", + "text": "백엔드 설정 파일에서 {property}를 {value}로 바꿔야 합니다, {documentation}를 참고하세요." + }, + "captcha": { + "kocaptcha": "KoCaptcha", + "native": "내장" + }, + "instance": { + "registrations": "유저 가입", + "captcha_header": "캡차", + "kocaptcha": "KoCaptcha 설정", + "access": "인스턴스 접근", + "restrict": { + "timelines": "타임라인 접근", + "profiles": "사용자 프로필 접근", + "activities": "게시물/활동 접근", + "header": "로그인하지 않은 방문자의 접근을 제한", + "description": "특정 API의 접근을 허용할지 말지에 대한 세부 설정입니다. 기본적으로(애매한 체크 표시) 인스턴스가 비공개이면 접근을 차단합니다, 체크 표시는 인스턴스가 공개여도 차단함을 의미합니다, 체크 해제는 인스턴스가 비공개여도 접근을 허용함을 의미합니다. 설정을 바꾸면 예기치 않은 동작이 일어날 수 있음을 유의하세요, 예로 프로필 접근이 차단되면 프로필 정보 없이 게시물이 보여집니다." + }, + "instance": "인스턴스 정보" + }, + "limits": { + "arbitrary_limits": "임의 제한", + "posts": "게시물 제한", + "uploads": "첨부파일 제한", + "users": "사용자 프로필 제한", + "profile_fields": "프로필 필드 제한", + "user_uploads": "프로필 미디어 제한" + }, + "frontend": { + "repository": "리포지토리 링크", + "versions": "사용 가능한 버전", + "build_url": "빌드 URL", + "reinstall": "재설치", + "is_default": "(기본)", + "is_default_custom": "(기본, 버전: {version})", + "install": "설치", + "install_version": "설치된 버전 {version}", + "more_install_options": "설치 옵션 더 보기", + "more_default_options": "기본 설정 옵션 더 보기", + "set_default": "기본으로 설정", + "set_default_version": "버전 {version}을 기본으로 설정", + "wip_notice": "이 부분은 프론트엔드 관리에 대한 백엔드 구현이 미완성이기 때문에 개발 중이고 몇몇 기능이 빠져 있습니다.", + "default_frontend": "기본 프론트엔드", + "default_frontend_tip2": "개발 중: 아직 Pleroma 백엔드가 모든 설치된 프론트엔드 목록을 알려주지 않기 때문에 이름과 ref을 직접 입력해야 합니다. 아래에 있는 목록은 여기 값을 입력하기 위한 단축 버튼입니다.", + "available_frontends": "설치 가능", + "default_frontend_tip": "기본 프론트엔드는 모든 유저에게 보입니다. 현재로썬 유저가 개인적으로 프론트엔드를 선택할 수 있진 않습니다. PleromaFE에서 벗어난다면 저희가 완전히 대체할 때까지는 인스턴스 설정을 위해서 아마도 낡고 버그투성이인 AdminFE를 쓰셔야 할 겁니다." + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":public": { + "label": "인스턴스를 공개", + "description": "이것을 끄면 모든 API가 로그인한 유저만 사용 가능하게 되며, 로그인하지 않은 사용자에겐 공개와 연합 타임라인이 보이지 않게 됩니다." + }, + ":limit_to_local_content": { + "label": "로컬 컨텐츠만 검색하도록 제한", + "description": "로그인하지 않은 사람 (기본값), 모두 또는 없음에게 전역 검색을 비활성화합니다" + }, + ":description_limit": { + "label": "글자수 제한", + "description": "첨부파일 설명문의 글자수 제한" + }, + ":background_image": { + "label": "배경 이미지", + "description": "배경 이미지 (주로 PleromaFE에서 쓰임)" + } + } + } + } } } diff --git a/src/i18n/nan-TW.json b/src/i18n/nan-TW.json @@ -189,7 +189,8 @@ "mobile_notifications": "拍開通知(有無讀ê)", "mobile_notifications_close": "關掉通知", "announcements": "公告", - "search": "Tshuē" + "search": "Tshuē", + "mobile_notifications_mark_as_seen": "Lóng 標做有讀" }, "notifications": { "broken_favorite": "狀態毋知影,leh tshiau-tshuē……", @@ -205,7 +206,13 @@ "migrated_to": "移民到", "reacted_with": "顯出{0} ê 反應", "submitted_report": "送出檢舉", - "poll_ended": "投票結束" + "poll_ended": "投票結束", + "unread_announcements": "{num} 篇公告iáu bē 讀", + "unread_chats": "{num} ê開講iáu bē讀", + "unread_follow_requests": "{num}ê新ê跟tuè請求", + "configuration_tip": "用{theSettings},lí通自訂siánn物佇tsia顯示。{dismiss}", + "configuration_tip_settings": "設定", + "configuration_tip_dismiss": "Mài koh顯示" }, "polls": { "add_poll": "開投票", @@ -248,12 +255,12 @@ "regional_indicator": "地區指引 {letter}" }, "errors": { - "storage_unavailable": "Pleroma buē-tàng the̍h 著瀏覽器儲存 ê。Lí ê 登入狀態抑是局部設定 buē 儲存,mā 凡勢 tú 著意料外 ê 問題。拍開 cookie 看覓。" + "storage_unavailable": "Pleroma buē-tàng the̍h 著瀏覽器儲存 ê。Lí ê 登入狀態抑是局部設定 buē 儲存,mā 凡勢 tú 著意料外 ê 問題。拍開 cookie 看māi。" }, "interactions": { "favs_repeats": "轉送 kap kah 意", "follows": "最近綴 lí ê", - "emoji_reactions": "繪文字 ê 回應", + "emoji_reactions": "繪文字 ê 反應", "reports": "檢舉", "moves": "用者 ê 移民", "load_older": "載入 koh khah 早 ê 互動" @@ -273,13 +280,13 @@ }, "content_type_selection": "貼 ê 形式", "content_warning": "主旨(毋是必要)", - "default": "Tú 正 kàu 高雄 ah。", + "default": "Tú正kàu高雄ah。", "direct_warning_to_all": "Tsit ê PO 文通 hōo 逐 ê 提起 ê 用者看見。", - "direct_warning_to_first_only": "Tsit ê PO 文,kan-ta 短信 tú 開始提起 ê 用者,tsiah 通看見。", + "direct_warning_to_first_only": "Tsit ê PO 文,kan-ta佇短phue tú開始提起ê用者,tsiah通看見。", "edit_remote_warning": "別 ê 站臺可能無支援編輯,無法度收著 PO 文上新 ê 版本。", "edit_unsupported_warning": "Pleroma 無支持編輯 the̍h 起 hām 投票。", "posting": "PO 文", - "preview": "Sing 看覓", + "preview": "Sing看māi", "preview_empty": "空 ê", "empty_status_error": "無法度 PO 無檔案 koh 空 ê 狀態", "media_description_error": "更新媒體失敗,請 koh 試一 kái", @@ -295,7 +302,9 @@ "public": "公開 - PO kàu 公開時間線", "unlisted": "Mài 列出來 - Mài PO tī 公開時間線" }, - "post": "PO 上去" + "post": "PO 上去", + "reply_option": "應tsit ê狀態", + "quote_option": "引用tsit ê狀態" }, "registration": { "bio_optional": "介紹(毋是必要)", @@ -360,7 +369,16 @@ "color": "色彩", "opacity": "無透明度", "contrast": { - "hint": "色彩ê對比率:{ratio}。{level}、 {context}" + "hint": "色彩ê對比率:{ratio}。{level}、 {context}", + "level": { + "aa": "合AA級ê準則(上kē ê)", + "aaa": "合AAA級ê準則(建議ê)", + "bad": "無合半ê無障礙準則" + }, + "context": { + "18pt": "大(18pt 以上)ê文字", + "text": "文字" + } } }, "switcher": { @@ -370,7 +388,7 @@ "keep_roundness": "保留邊á角ê khà-buh", "keep_fonts": "保持字型", "reset": "重頭設定", - "clear_all": "攏清掉", + "clear_all": "Lóng清掉", "clear_opacity": "清掉無透明度", "load_theme": "載入主題", "keep_as_is": "Mài振動", @@ -380,8 +398,116 @@ "upgraded_from_v2": "PleromaFE升級ah,主題huân-sè kap lí知影ê無kâng。", "v2_imported": "Lí輸入ê檔案是舊版本ê前端用ê。Guán盡量予版本相通,毋過可能有所在buē-tàng。", "older_version_imported": "Lí輸入ê檔案是予舊ê前端用ê。", - "future_version_imported": "Lí輸入ê檔案是新ê前端所用ê。" + "future_version_imported": "Lí輸入ê檔案是新ê前端所用ê。", + "snapshot_missing": "無主題ê快相佇檔案內,所以,伊看起來凡勢kap原來預料ê無kâng。", + "snapshot_present": "主題ê快相有載入,所以逐ê值lóng khàm過去ah。Lí 通改載入主題實際ê資料。", + "fe_upgraded": "版本更新了後,Pleroma前端ê ia̋n-jín 升級ah。", + "fe_downgraded": "Pleroma ê前端滾tńg去ah。", + "migration_snapshot_ok": "為著保險,主題快相載入去ah。Lí ē當試載入主題資料。", + "migration_napshot_gone": "快相因故無去ah,tsi̍t-kuá所在看起來可能hām lí所想ê無kâng。", + "snapshot_source_mismatch": "版本tshia̋ng-póng:上可能因為前端滾轉去koh更新ah,若因為用舊版本ê前端,主題tsiah改變,lí有可能beh用舊ê版本。無,著用新ê。" + }, + "save_load_hint": "佇揀iah是載入主題ê時,「保存」選項保留現tsú時設定ê選項;mā佇輸出主題ê時tsūn,儲存頭拄á講ê選項。若是逐ê選擇框á無設定,逐項設定就ē khǹg佇輸出ê主題。" + }, + "common_colors": { + "_tab_label": "一般", + "main": "一般ê色彩", + "foreground_hint": "請看「進階」分頁,來調整khah幼ê所在", + "rgbo": "標頭、強調、徽章" + }, + "advanced_colors": { + "_tab_label": "進階", + "alert": "警告ê背景", + "alert_error": "錯誤", + "alert_warning": "警告", + "alert_neutral": "其他ê", + "post": "PO文/用者紹介", + "badge": "徽章ê背景", + "popover": "提示、目錄、跳出來ê", + "badge_notification": "通知", + "panel_header": "面枋ê標題", + "top_bar": "頂 liâu-á", + "borders": "框á邊", + "buttons": "鈕仔", + "inputs": "輸入框á", + "faint_text": "淺ê文字", + "underlay": "Tshū-á", + "wallpaper": "壁紙", + "poll": "投票數ê圖", + "icons": "標á", + "highlight": "強調ê要素", + "pressed": "Tshi̍h ê 時", + "selectedPost": "選擇ê PO文", + "selectedMenu": "選擇ê目錄項目", + "disabled": "關ê", + "toggled": "切換ê時", + "tabs": "分頁", + "chat": { + "incoming": "收著ê", + "outgoing": "送出ê", + "border": "框á邊" + } + }, + "radii": { + "_tab_label": "邊á角ê khà-buh" + }, + "shadows": { + "_tab_label": "影kap光", + "override": "Khàm掉", + "shadow_id": "影 #{value}", + "blur": "予n̄g-n̄g", + "spread": "Hōo 闊", + "inset": "內pîng", + "filter_hint": { + "always_drop_shadow": "警告,tsit ê 影一直用 {0},若是瀏覽器支援tsē。", + "drop_shadow_syntax": "{0} 無支援參數 {1} kap 關鍵字 {2}。", + "avatar_inset": "請注意,結合內pîng kap外pîng ê影佇標頭,可能佇透明ê標頭現無預料ê結果。", + "spread_zero": "若是「hōo 闊」ê值比0較大,影ê顯示ē kap hōo 闊設做0 kâng款", + "inset_classic": "內pîng ê影ē用{0}" + }, + "component": "部件", + "hintV3": "針對影,lí mā ē當用 {0} 標示法,來用其他ê色彩 khang (slot)。", + "components": { + "panelHeader": "面枋ê標題", + "topBar": "頂 liâu-á", + "avatar": "用者ê標頭(佇個人資料欄位)", + "popup": "跳出來ê kap提醒", + "button": "鈕仔", + "buttonHover": "鈕仔(滑鼠ê指標khǹg佇面頂)", + "panel": "面枋", + "avatarStatus": "用者ê標頭(佇PO文ê顯示)", + "buttonPressedHover": "鈕仔(滑鼠指標leh khǹg 佇頂懸,koh tshi̍h ê時)", + "buttonPressed": "鈕仔(leh tshi̍h ê時)", + "input": "輸入框á" } + }, + "fonts": { + "_tab_label": "字型", + "components": { + "interface": "界面", + "input": "輸入框á", + "post": "PO文", + "postCode": "RTF ê PO文ê平闊文字" + }, + "family": "字型ê名", + "help": "揀界面元件所用ê字型。若是揀「家己指定」,lí著輸入系統內ê字型正確ê名。", + "size": "Sài-suh(單位:畫素)", + "weight": "字ê重(粗度)", + "custom": "家己指定" + }, + "preview": { + "header": "先看māi", + "content": "內容", + "error": "錯誤ê例", + "button": "鈕á", + "text": "Tsē是{0}kap{1} ê例", + "mono": "內容", + "input": "Tú正kàu高雄ah。", + "faint_link": "有幫tsān ê手冊", + "fine_print": "讀guán ê {0},毋過學無有路用ê!", + "header_faint": "Tsē OK", + "checkbox": "我有讀過使用條款", + "link": "好ê細ê連結" } }, "upload": { @@ -594,11 +720,11 @@ "hide_all_muted_posts": "Khàm掉消音êPO文", "max_thumbnails": "PO文ê縮小圖ê khòo-tah(無寫=無限制)", "hide_isp": "Khàm 站臺特有ê面 pang", - "right_sidebar": "Kā 邊á liâu徙kah正手pîng", - "navbar_column_stretch": "伸導覽liâu,kah 欄平闊", + "right_sidebar": "Kā 邊á ê欄位徙kah正手pîng", + "navbar_column_stretch": "伸導覽liâu,kah 欄位平闊", "always_show_post_button": "一直顯示「新ê PO文」ê鈕仔", "hide_wallpaper": "Khàm站臺ê壁紙", - "use_one_click_nsfw": "Tshi̍h 一ê就會當拍開敏感內容", + "use_one_click_nsfw": "Tshi̍h chi̍t 下就ē當拍開敏感內容", "hide_post_stats": "Khàm PO文ê統計數據(比如:kah 意ê額數)", "hide_filtered_statuses": "Khàm 逐ê過濾掉êPO文", "hide_wordfiltered_statuses": "Khàm詞語過濾掉ê狀態", @@ -627,7 +753,7 @@ "mute_bot_posts": "Kā 機器lâng ê PO文消音", "hide_shoutbox": "Khàm 站臺ê留話pang", "account_backup_description": "Tse 予 lí ē當 kā lín 口座 ê 資訊 kap PO 文載落來,毋過 in 猶無法度輸入kàu Pleroma口座 ê 內底。", - "theme_help_v2_1": "拍開選擇框á就 ē 當改掉一寡組件ê色彩kap無透明度。Ji̍h「清掉所有ê」,ē 恢復原來ê款。", + "theme_help_v2_1": "拍開選擇框á就 ē 當改掉一寡組件ê色彩kap無透明度。Ji̍h「Lóng清掉」,ē 恢復原來ê款。", "preload_images": "Kā 圖片先載入", "hide_user_stats": "Khàm 掉用者ê統計數據(比如:綴ê lâng額)", "interfaceLanguage": "界面ê語言", @@ -678,7 +804,7 @@ "notification_visibility_mentions": "提起", "notification_visibility_repeats": "轉送", "notification_visibility_moves": "用者suá位", - "notification_visibility_emoji_reactions": "回應", + "notification_visibility_emoji_reactions": "反應", "notification_visibility_polls": "Lí參與ê選舉辦suah佇", "no_rich_text_description": "Po文mài用RTF格式", "no_blocks": "無封鎖", @@ -690,7 +816,7 @@ "show_moderator_badge": "佇我ê個人資料顯示「管理員」證章", "nsfw_clickthrough": "Khàm掉敏感ê媒體內容", "oauth_tokens": "OAuth token", - "refresh_token": "對頭the̍h token", + "refresh_token": "重頭the̍h token", "valid_until": "到期佇", "revoke_token": "撤回", "panelRadius": "面pang", @@ -716,12 +842,12 @@ "set_new_avatar": "設定新ê標頭", "set_new_profile_background": "設定新ê個人資料ê背景", "set_new_profile_banner": "設定新ê個人資料ê條á", - "reset_avatar": "Tuì頭設定標頭", - "reset_profile_background": "Tuì頭設個人資料ê背景", - "reset_profile_banner": "Tuì頭設個人資料ê條á", - "reset_avatar_confirm": "Lí敢確實beh tuì頭設定標頭?", - "reset_banner_confirm": "Lí敢確實beh tuì頭設定條á?", - "reset_background_confirm": "Lí敢確實beh tuì頭設定背景?", + "reset_avatar": "重頭設定標頭", + "reset_profile_background": "重頭設個人資料ê背景", + "reset_profile_banner": "重頭設個人資料ê條á", + "reset_avatar_confirm": "Lí敢確實beh 重頭設定標頭?", + "reset_banner_confirm": "Lí敢確實beh 重頭設定條á?", + "reset_background_confirm": "Lí敢確實beh 重頭設定背景?", "settings": "設定", "subject_input_always_show": "一直顯示主旨ê格á", "subject_line_behavior": "回應ê時,khóo-pih主旨", @@ -731,11 +857,11 @@ "conversation_display": "顯示對話ê風格", "conversation_display_tree": "樹á ê形", "disable_sticky_headers": "Mài 予欄位ê頭牢佇螢幕頂懸", - "show_scrollbars": "展示邊á liâu ê giú-á", + "show_scrollbars": "展示邊á ê欄位 ê giú-á", "third_column_mode": "空間夠額ê時,展示第三ê欄位", "third_column_mode_none": "不管時mài顯示第三ê欄位", "third_column_mode_notifications": "通知ê欄位", - "third_column_mode_postform": "主要êPO文表kah導覽", + "third_column_mode_postform": "主要ê PO文表kah導覽", "show_admin_badge": "佇我ê個人資料顯示「行政員」證章", "pause_on_unfocused": "若是 Pleroma ê分頁無點開,tiō 暫停更新", "conversation_display_tree_quick": "樹á形ê展示", @@ -792,15 +918,437 @@ "notification_mutes": "若tsún無愛收tuì指定用者來ê通知,著用消音。", "notification_blocks": "封鎖用者ē停止所有i hia來ê通知,mā取消訂伊。", "enable_web_push_notifications": "拍開網頁sak通知ê功能", - "more_settings": "Koh較tsē ê設定" + "more_settings": "Koh較tsē ê設定", + "version": { + "title": "版本", + "backend_version": "後端ê版本", + "frontend_version": "前端ê版本" + }, + "commit_value": "儲存", + "commit_value_tooltip": "值無儲存,tshi̍h tsit ê 鈕仔來送出你改變ê", + "hard_reset_value": "硬ê重頭設", + "hard_reset_value_tooltip": "Suá掉儲存內底ê設定,強制用預設ê值", + "reset_value": "重頭設", + "reset_value_tooltip": "重頭設草稿", + "hide_scrobbles": "Tshàng scrobble(記錄)", + "notification_show_extra": "顯示koh khah tsē ê通知佇通知ê欄位", + "notification_extra_chats": "顯示bô讀ê開講", + "notification_extra_announcements": "顯示bô讀ê公告", + "notification_extra_follow_requests": "顯示新ê跟tuè請求", + "notification_extra_tip": "顯示自訂其他通知ê撇步" }, "status": { - "favorites": "收藏" + "favorites": "收藏", + "repeat_confirm_cancel_button": "Mài轉送", + "delete_confirm_title": "Thâi掉ê確認", + "edit": "編輯狀態", + "edited_at": "(頂kái編輯佇:{time})", + "pin": "釘佇個人資料", + "unpin": "Tuì個人資料拆掉", + "pinned": "釘入去ê", + "bookmark": "加入冊籤", + "unbookmark": "Tuì冊籤the̍h掉", + "delete_confirm": "Lí kám真ê beh thâi掉tsit ê狀態?", + "delete_confirm_accept_button": "Thâi掉", + "delete_confirm_cancel_button": "保留", + "reply_to": "回應", + "replies_list": "回應:", + "repeats": "轉送", + "repeat_confirm_accept_button": "轉送", + "repeat_confirm_title": "轉送ê確認", + "repeat_confirm": "Lí kám真ê beh轉送tsit ê狀態?", + "delete": "Thâi掉身份", + "delete_error": "Thâi狀態ê時出tshê:{0}", + "mentions": "提起", + "move_down": "Kā附件suá kàu正pîng", + "thread_show_full": "展示tsit 條討論線ê所有(lóng總有{numStatus}ê狀態,深度上限:{depth})", + "thread_follow": "看討論線tshun ê部份(lóng總有{numStatus}ê狀態)", + "replies_list_with_others": "回應(+其他{numReplies}ê):", + "mute_conversation": "Kā會話消音", + "unmute_conversation": "Kā會話取消消音", + "status_unavailable": "狀態bē當用", + "copy_link": "Khóo-pih 狀態ê連結", + "external_source": "外口ê來源", + "thread_muted": "討論線消音ah", + "thread_muted_and_words": ",有詞語:", + "hide_full_subject": "Khàm掉主題ê全文", + "show_full_subject": "顯示標題ê全文", + "show_content": "顯示內容", + "hide_content": "Khàm掉內容", + "status_deleted": "Tsit篇PO文thâi掉ah", + "nsfw": "敏感ê內容", + "expand": "Thián開", + "you": "(Lí)", + "plus_more": "Koh有{number}ê", + "many_attachments": "PO文有{number}ê附件", + "collapse_attachments": "Kā附件tshàng起來", + "show_all_attachments": "顯示逐ê附件", + "show_attachment_in_modal": "佇媒體模式顯示", + "show_attachment_description": "Kā敘述先看māi(拍開附件會當看kui ê敘述)", + "hide_attachment": "Khàm掉附件", + "attachment_stop_flash": "停止Flash ê播放器", + "remove_attachment": "Kā附件suá走", + "move_up": "Kā附件suá kàu倒pîng", + "open_gallery": "拍開畫廊", + "thread_hide": "Khàm掉討論線", + "thread_show": "顯示討論線", + "thread_show_full_with_icon": "{icon} {text}", + "thread_follow_with_icon": "{icon} {text}", + "ancestor_follow": "看其他{numReplies}ê佇tsit ê狀態ê回應", + "ancestor_follow_with_icon": "{icon} {text}", + "show_all_conversation_with_icon": "{icon} {text}", + "show_all_conversation": "看kui ê會話(有其他{numStatus}ê狀態)", + "show_only_conversation_under_this": "Kan-ta顯示tsit ê狀態ê回應", + "status_history": "狀態ê歷史", + "reaction_count_label": "{num}ê lâng用表情反應", + "hide_quote": "Khàm條引用ê狀態", + "display_quote": "顯示引用ê狀態", + "invisible_quote": "引用ê狀態bē當用:{link}", + "more_actions": "佇tsit ê狀態ê其他動作" }, "user_card": { - "favorites": "收藏" + "favorites": "收藏", + "show_repeats": "顯示轉送", + "hide_repeats": "Khàm掉轉送", + "remove_follower_confirm": "Lí kám真正想beh kā {user} tuì lí所跟綴ê suá走?", + "statuses": "狀態", + "admin_menu": { + "activate_account": "啟動口座", + "deactivate_account": "予口座失效", + "delete_account": "Thâi掉口座", + "force_nsfw": "Kā逐ê PO文標做敏感內容", + "strip_media": "Tuì PO文thâi掉媒體", + "force_unlisted": "強制PO文mài列佇公共時間線", + "disable_remote_subscription": "Mài允准tuì其他站臺跟tuè用者", + "sandbox": "強制PO文kan-ta予跟tuè ê看", + "disable_any_subscription": "Mài允准跟tuè任何用者", + "quarantine": "Tuì聯邦禁止用者ê PO文", + "delete_user": "Thâi掉用者ê口座", + "delete_user_data_and_deactivate_confirmation": "Án-ne ē永永thâi掉tsit ê口座ê資料兼hōo失效。Lí kám完全確定?", + "grant_admin": "授與行政員ê權", + "revoke_admin": "撤掉行政員ê權", + "moderation": "仲裁", + "grant_moderator": "授與仲裁員ê權", + "revoke_moderator": "撤掉仲裁員ê權" + }, + "highlight": { + "disabled": "Mài強調", + "side": "邊á ê花tsuā", + "solid": "孤色ê背景", + "striped": "花tsuā ê背景" + }, + "note": "筆記", + "note_blank": "(無)", + "edit_note": "編輯筆記", + "edit_note_apply": "適用", + "approve": "核准", + "approve_confirm_title": "核准ê確認", + "approve_confirm_accept_button": "核准", + "approve_confirm_cancel_button": "Mài核准", + "block": "封鎖", + "blocked": "封鎖ah!", + "block_confirm_title": "封鎖ê確認", + "approve_confirm": "Lí kám想beh核准{user}ê跟tuè請求?", + "block_confirm": "Lí kám 真正想beh封鎖{user}?", + "block_confirm_accept_button": "封鎖", + "block_confirm_cancel_button": "Mài封鎖", + "deactivated": "停止使用ah", + "deny": "拒絕", + "deny_confirm_title": "拒絕ê確認", + "deny_confirm_accept_button": "拒絕", + "deny_confirm_cancel_button": "Mài拒絕", + "deny_confirm": "Lí kám想beh拒絕{user}ê跟tuè請求?", + "edit_profile": "編輯個人資料", + "follow": "跟tuè", + "follow_cancel": "取消請求", + "follow_sent": "請求送ah!", + "follow_progress": "Teh請求……", + "follow_unfollow": "無愛跟tuè", + "unfollow_confirm_title": "無愛跟tuè ê確認", + "unfollow_confirm": "lí kám真正無beh跟tuè {user}?", + "unfollow_confirm_accept_button": "無愛跟綴", + "unfollow_confirm_cancel_button": "繼續跟tuè", + "followees": "Teh跟綴", + "followers": "跟綴ê", + "following": "Teh跟tuè!", + "follows_you": "跟tuè lí!", + "hidden": "Tshàng起來ê", + "its_you": "Tse是lí!", + "media": "媒體", + "mention": "提起", + "message": "短phue", + "mute": "消音", + "muted": "消音ê", + "mute_confirm_title": "消音ê確認", + "mute_confirm": "Lí確定想beh kā {user}消音?", + "mute_confirm_accept_button": "消音", + "mute_confirm_cancel_button": "Mài消音", + "mute_duration_prompt": "消音tsit ê用戶ê期限(0表示永遠):", + "per_day": "/kang", + "remote_follow": "遠距離ê關注", + "remove_follower": "Suá走跟綴ê", + "remove_follower_confirm_title": "Suá走跟tuè者ê確認", + "remove_follower_confirm_accept_button": "Suá走", + "remove_follower_confirm_cancel_button": "保留", + "report": "檢舉", + "subscribe": "注文", + "unsubscribe": "取消注文", + "unblock": "Mài封鎖", + "unblock_progress": "Teh取消封鎖……", + "block_progress": "Leh封鎖……", + "unmute": "Mài消音", + "mute_progress": "Leh消音……", + "unmute_progress": "Leh取消消音……", + "bot": "機器lâng", + "birthday": "出世佇{birthday}", + "edit_note_cancel": "取消" }, "tool_tip": { - "favorite": "收藏" + "favorite": "收藏", + "repeat": "轉送", + "media_upload": "Kā媒體傳起去", + "reply": "回應", + "add_reaction": "加反應", + "user_settings": "用者ê設定", + "accept_follow_request": "允准跟tuè ê請求", + "reject_follow_request": "拒絕跟tuè ê請求", + "bookmark": "冊籤", + "toggle_expand": "Thián開á是tshàng通知,顯示kui篇PO文", + "toggle_mute": "Thián開á是tshàng通知,顯露消音ê內容", + "autocomplete_available": "{number} ê結果通用。用頂kap下ê key來看結果。" + }, + "password_reset": { + "instruction": "輸入你ê email地址iah是用者ê名。阮ē寄予lí連結,通重頭設你ê密碼。", + "password_reset_disabled": "密碼重頭設ê功能無開放。請聯絡lín站臺ê行政員。", + "password_reset_required_but_mailer_is_disabled": "Lí著重設密碼,M̄-koh重頭設密碼ê功能無開放。請聯絡lín站臺ê行政員。", + "forgot_password": "Buē記得密碼?", + "password_reset": "密碼重頭設", + "placeholder": "你ê email iah是用者ê名", + "check_email": "檢查你ê電子phue箱,有重頭設密碼ê連結ê phue無。", + "return_home": "Tńg去頭頁", + "too_many_requests": "Lí已經kàu 試ê回數限制 ah,小等leh koh試。", + "password_reset_required": "Lí著重設密碼,tsiah通登入。" + }, + "admin_dash": { + "window_title": "行政員", + "reset_all": "Kui ê重頭設", + "wip_notice": "Tsit ê 管理 la-jí-báng (dashboard) 是試驗ê,koh teh 起做,{adminFeLink}.", + "old_ui_link": "舊ê管理界面佇tsia", + "commit_all": "Lóng總儲存", + "tabs": { + "nodb": "無資料庫ê設置", + "instance": "站臺", + "limits": "限制", + "frontends": "前端" + }, + "nodb": { + "heading": "資料庫設置無開放", + "text": "Lí需要改後端ê設置檔案,tsiah ē當kā{property}設做{value},請佇{documentation}了解詳細。", + "documentation": "文件", + "text2": "大部份ê設定ē無開放。" + }, + "limits": { + "user_uploads": "個人資料ê媒體限制", + "arbitrary_limits": "任何限制", + "posts": "PO文ê限制", + "uploads": "附件ê限制", + "users": "用者個人資料ê限制", + "profile_fields": "個人資料欄位ê限制" + }, + "captcha": { + "native": "在來ê", + "kocaptcha": "KoCaptcha" + }, + "instance": { + "instance": "站臺ê資訊", + "registrations": "用者ê註冊", + "kocaptcha": "KoCaptcha ê設定", + "restrict": { + "header": "管制無落名ê訪客使用", + "timelines": "讀取時間線", + "profiles": "讀取用者ê個人資料", + "activities": "讀取狀態/活動", + "description": "(無)允准一kuá方面ê API the̍h取資源ê詳細設定。預設(無定ê狀態),若是站臺毋是公開ê,ē無允准the̍h取;選擇框á若勾,就算站臺是公開ê,iáu是無允准the̍h取;若無勾,就算站臺是私人ê,mā是允准the̍h取。請注意,若是設一kuá設定,無預料ê行為可能產生。比如講,若是the̍h取個人資料無開放,PO文buē顯示個人資料。" + }, + "access": "讀取實體", + "captcha_header": "CAPTCHA" + }, + "frontend": { + "repository": "原始碼庫ê連結", + "versions": "通用ê版本", + "build_url": "起做URL", + "reinstall": "重頭安裝", + "is_default": "(預設)", + "is_default_custom": "(預設,版本:{version})", + "install": "安裝", + "install_version": "安裝ê版本:{version}", + "more_install_options": "其他ê安裝選項", + "more_default_options": "其他ê預設設定ê選項", + "set_default": "設做預設ê", + "set_default_version": "Kā版本{version}設做預設ê", + "default_frontend_tip": "預設ê前端ē展示予逐ê用者。現在,用者無法度揀個人ê前端。若是lí變換,無beh用PleromaFE,上有可能ē用舊koh問題tsē ê AdminFE 做站臺ê設置,佇阮iáu-bē kā伊取代以前。", + "wip_notice": "請注意,tsit ê段落iáu teh起做,欠缺一寡特點,因為後端tuì前端管理ê實做無齊備。", + "default_frontend": "預設ê前端", + "default_frontend_tip2": "Teh起做:因為Pleroma後端無適當列出逐ê安裝ê前端,lí著手動輸入名字kap引用。下kha ê列單提供寫tsiah-ê 值ê近路。", + "available_frontends": "Ē當安裝" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":public": { + "description": "無開放tse,ē 控制逐êAPI,干焦予登入ê用者用,mā ē予公開kap聯邦ê時間線,buē當予無落名ê訪客the̍h著。", + "label": "站臺是公開ê" + }, + ":limit_to_local_content": { + "label": "Kan-ta會當tshuē在地ê內容", + "description": "無開放無認證ê用者、逐儂,猶是lóng總開放tshuē全球ê網路" + }, + ":description_limit": { + "label": "限制", + "description": "附件說明ê字元限制" + }, + ":background_image": { + "label": "背景ê影像", + "description": "背景ê影像(主要予PleromaFE用)" + } + } + } + } + }, + "timeline": { + "up_to_date": "是上新ê", + "collapse": "疊起來", + "conversation": "會話", + "error": "佇the̍h時間線ê時出tshê:{0}", + "load_older": "載入舊ê狀態", + "repeated": "轉送ah", + "no_retweet_hint": "PO文hőng標做限定跟綴ê,á是私人phue,無法度轉送", + "show_new": "看新ê", + "reload": "重新載入", + "no_more_statuses": "無其他ê狀態", + "no_statuses": "無狀態", + "socket_reconnected": "實時ê連結成立ah", + "socket_broke": "實時連結拍m̄見ah:CloseEvent代碼{0}", + "quick_view_settings": "快速 view ê設定", + "quick_filter_settings": "快速過濾器ê設定" + }, + "time": { + "unit": { + "days": "{0}工", + "days_short": "{0}工", + "hours": "{0}點鐘", + "hours_short": "{0}點鐘", + "minutes": "{0}分鐘", + "minutes_short": "{0}分", + "months": "{0}個月", + "months_short": "{0}個月", + "seconds": "{0}秒鐘", + "seconds_short": "{0}秒", + "weeks": "{0}禮拜", + "weeks_short": "{0}週", + "years": "{0}年", + "years_short": "{0}年" + }, + "in_future": "koh有{0}", + "in_past": "{0}進前", + "now": "tú正", + "now_short": "tsit-má" + }, + "user_reporting": { + "title": "檢舉 {0}", + "forward_description": "Tsit ê口座是別ê站臺ê。Mā kám beh寄報告ê khóo-pih kàu hit ê站?", + "add_comment_description": "本檢舉ē 寄kàu你ê站臺ê仲裁員。Lí會當佇下kha解說檢舉tsit ê口座ê原因:", + "additional_comments": "其他ê意見", + "forward_to": "轉送kàu{0}", + "submit": "送出", + "generic_error": "佇處理lí ê請求ê時出tshê。" + }, + "lists": { + "really_delete": "Kám真正 beh thâi列單?", + "search": "Tshiau-tshuē用者", + "create": "建立", + "save": "保存改變", + "delete": "Thâi列單", + "lists": "列單", + "new": "新ê列單", + "title": "列單ê標題", + "following_only": "限制佇跟tuè ê", + "manage_lists": "管理列單", + "manage_members": "管理列單ê成員", + "add_members": "Tshiau-tshuē其他ê用者", + "remove_from_list": "Tuì列單suá走", + "add_to_list": "Ke-thinn kàu列單", + "is_in_list": "已經佇列單內底", + "editing_list": "編輯列單 {listTitle}", + "creating_list": "開新ê列單", + "update_title": "保存標題", + "error": "佇操作列單ê時出tshê:{0}" + }, + "update": { + "update_bugs": "請報告任何問題kap錯誤佇 {pleromaGitlab},因為已經改變真tsē。雖bóng guán徹底試過,ka-kī mā用開發版,iáu是有可能有無注意ê所在。Guán歡迎lí tuì所tú tio̍h ê問題,提出意見kap建議,或者是改進Pleroma kap Pleroma-FE ê方法。", + "big_update_title": "請sió等tsi̍t ê", + "update_bugs_gitlab": "Pleroma GitLab", + "big_update_content": "Guán已經有tsi̍t段時間無推出發行,所以外觀kap感覺kap lí所慣勢ê,凡勢無kâng。", + "update_changelog": "Beh知影改變ê詳細,請看{theFullChangelog}。", + "update_changelog_here": "Changelog全文", + "art_by": "美術製作:{linkToArtist}" + }, + "user_profile": { + "timeline_title": "用者ê時間線", + "profile_does_not_exist": "Pháinn勢,tsit ê個人資料無佇leh。", + "profile_loading_error": "Pháinn勢,佇載入tsit ê個人資料ê時出tshê。" + }, + "who_to_follow": { + "more": "詳情", + "who_to_follow": "Siáng通tuè" + }, + "upload": { + "error": { + "base": "傳起去ê時失敗。", + "message": "傳起去ê時失敗:{0}", + "file_too_big": "檔案siūnn大[{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Koh試tsi̍t kái" + }, + "file_size_units": { + "B": "B", + "KiB": "KiB", + "MiB": "MiB", + "GiB": "GiB", + "TiB": "TiB" + } + }, + "search": { + "people": "Lâng", + "hashtags": "井字ê標籤", + "person_talking": "{count}ê lâng teh開講", + "people_talking": "{count}ê lâng teh開講", + "no_results": "無結果", + "no_more_results": "無其他結果", + "load_more": "載入其他結果" + }, + "chats": { + "you": "Lí:", + "message_user": "送短phue予:{nickname}", + "delete": "Thâi掉", + "chats": "開講", + "new": "新ê開講", + "empty_message_error": "Bē當PO空ê短phue", + "more": "其他", + "delete_confirm": "Lí kám真正beh thâi tsit ê短phue?", + "error_loading_chat": "佇載入開講ê時出問題。", + "error_sending_message": "佇送短phue ê時出問題。", + "empty_chat_list_placeholder": "Lí iáu buē開講過。開始開講!" + }, + "file_type": { + "audio": "聲音", + "video": "影片", + "image": "影像", + "file": "檔案" + }, + "display_date": { + "today": "今á日" + }, + "unicode_domain_indicator": { + "tooltip": "Tsit ê域名含m̄是ascii ê字元。" } } diff --git a/src/i18n/pt.json b/src/i18n/pt.json @@ -11,7 +11,8 @@ "title": "Características", "who_to_follow": "Quem seguir", "upload_limit": "Limite de carregamento", - "pleroma_chat_messages": "Chat do Pleroma" + "pleroma_chat_messages": "Chat do Pleroma", + "shout": "Shoutbox" }, "finder": { "error_fetching_user": "Erro ao pesquisar utilizador", @@ -36,11 +37,27 @@ "error_retry": "Por favor, tenta novamente", "loading": "A carregar…", "dismiss": "Ignorar", - "role": - { + "role": { "moderator": "Moderador", "admin": "Admin" - } + }, + "undo": "Refazer", + "yes": "Sim", + "no": "Não", + "unpin": "Desafixar o item", + "scroll_to_top": "Rolar para o topo", + "flash_content": "Clique para mostrar conteúdo Flash usando o Ruffle (Experimental, talvez não funcione).", + "flash_security": "Note que isso pode ser potencialmente perigoso dado que o conteúdo Flash ainda é código arbitrário.", + "flash_fail": "Falha ao carregar conteúdo flash, veja o console para detalhes.", + "scope_in_timeline": { + "direct": "Direct", + "private": "Apenas-seguidores", + "public": "Público", + "unlisted": "Não-listado" + }, + "pin": "Fixar o item", + "generic_error_message": "Um erro ocorreu: {0}", + "never_show_again": "Não mostrar mais" }, "image_cropper": { "crop_picture": "Cortar imagem", @@ -64,11 +81,17 @@ "recovery_code": "Código de recuperação", "authentication_code": "Código de autenticação", "enter_two_factor_code": "Introduza o código de dois fatores", - "enter_recovery_code": "Introduza um código de recuperação" + "enter_recovery_code": "Introduza um código de recuperação", + "logout_confirm_title": "Confirmação de logoff", + "logout_confirm": "Você realmente quer sair?", + "logout_confirm_accept_button": "Sair", + "logout_confirm_cancel_button": "Não sair" }, "media_modal": { "previous": "Anterior", - "next": "Próximo" + "next": "Próximo", + "counter": "{current} / {total}", + "hide": "Fechar visualizador de mídia" }, "nav": { "about": "Sobre", @@ -88,7 +111,18 @@ "administration": "Administração", "chats": "Salas de Chat", "timelines": "Cronologias", - "bookmarks": "Itens Guardados" + "bookmarks": "Itens Guardados", + "home_timeline": "Timeline da home", + "lists": "Listas", + "edit_pinned": "Editar itens fixados", + "edit_nav_mobile": "Customizar barra de navegação", + "mobile_notifications_mark_as_seen": "Marcar todas como vistas", + "search_close": "Fechar barra de busca", + "mobile_notifications_close": "Fechar notificações", + "announcements": "Anúncios", + "edit_finish": "Edição finalizada", + "mobile_sidebar": "Alternar barra lateral móvel", + "mobile_notifications": "Abrir notificações (há notificações não lidas)" }, "notifications": { "broken_favorite": "Publicação desconhecida, a procurar…", @@ -102,7 +136,15 @@ "reacted_with": "reagiu com {0}", "migrated_to": "migrou para", "follow_request": "quer seguir-te", - "error": "Erro ao obter notificações: {0}" + "error": "Erro ao obter notificações: {0}", + "unread_announcements": "{num} anúncio não lido | {num} anúncios não lidos", + "unread_chats": "{num} mensagem não lida | {num} mensagens não lidas", + "configuration_tip": "Você pode customizar o que você deseja mostrar aqui em {theSettings}. {dismiss}", + "unread_follow_requests": "{num} novo pedido de seguidor | {num} novos pedidos de seguidores", + "configuration_tip_settings": "as configurações", + "configuration_tip_dismiss": "Não mostrar novamente", + "poll_ended": "enquete finalizada", + "submitted_report": "enviado um relatório" }, "post_status": { "new_status": "Publicar nova publicação", @@ -136,7 +178,14 @@ "media_description": "Descrição da multimédia", "media_description_error": "Falha ao atualizar ficheiro, tente novamente", "direct_warning_to_first_only": "Esta publicação só será visível para os utilizadores mencionados no início da mensagem.", - "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados." + "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados.", + "edit_status": "Editar status", + "reply_option": "Responder a esse status", + "quote_option": "Citar esse status", + "edit_remote_warning": "Outras instâncias remotas talvez não suportem edição e sejam incapazes de receber a última versão do seu post.", + "content_type_selection": "Formato do post", + "scope_notice_dismiss": "Fechar essa notificação", + "edit_unsupported_warning": "Pleroma não suporta editar menções ou enquetes." }, "registration": { "bio": "Biografia", @@ -156,8 +205,18 @@ "email_required": "não pode ser deixado em branco", "password_required": "não pode ser deixado em branco", "password_confirmation_required": "não pode ser deixado em branco", - "password_confirmation_match": "deve corresponder à palavra-passe" - } + "password_confirmation_match": "deve corresponder à palavra-passe", + "birthday_required": "não pode ser deixado em branco", + "birthday_min_age": "deve ser em ou antes de {date}" + }, + "birthday": "Data de nascimento:", + "reason": "Razão para registrar", + "register": "Registrar", + "reason_placeholder": "Essa instância aprova os registros manualmente.\nPermita ao administrador saber o porquê do seu registro.", + "birthday_optional": "Data de nascimento (opcional):", + "bio_optional": "Bio (opcional)", + "email_optional": "Email (opcional)", + "email_language": "Em qual linguagem você deseja receber emails do servidor?" }, "settings": { "app_name": "Nome da aplicação", @@ -523,7 +582,56 @@ "autohide_floating_post_button": "Automaticamente ocultar o botão 'Nova Publicação' (telemóvel)", "notification_visibility_moves": "Utilizador Migrado", "accent": "Destaque", - "pad_emoji": "Preencher espaços ao adicionar emojis do seletor" + "pad_emoji": "Preencher espaços ao adicionar emojis do seletor", + "confirm_dialogs_logout": "saindo", + "move_account_error": "Erro ao mover conta: {error}", + "confirm_dialogs_delete": "excluindo um status", + "save": "Salvar mudanças", + "lists_navigation": "Mostrar listas na navegação", + "email_language": "Linguagem para receber emails do servidor", + "account_backup_description": "Isso permite a você baixar um arquivo das informações da sua conta e os seus posts, mas eles ainda não podem ser importados para uma conta do Pleroma.", + "add_backup_error": "Erro ao adicionar um novo backup: {error}", + "confirm_dialogs": "Pedir por confirmação quando", + "confirm_dialogs_repeat": "repetindo um status", + "account_alias": "Apelidos de conta", + "account_alias_table_head": "Apelido", + "list_aliases_error": "Erro ao buscar por apelidos: {error}", + "hide_list_aliases_error_action": "Fechar", + "confirm_dialogs_deny_follow": "negando um seguidor", + "confirm_dialogs_approve_follow": "aprovando um seguidor", + "backup_running": "Esse backup está em andamento, {number} registro processado. | Esse backup está em progresso, {number} registros processados.", + "add_backup": "Criar um novo backup", + "added_backup": "Adicionado um novo backup.", + "backup_failed": "Esse backup falhou.", + "list_backups_error": "Erro ao buscar a lista de backup: {error}", + "move_account_notes": "Se você deseja mover a conta para outro lugar, você deve ir para sua conta de destino e adicionar um apelido apontando para cá.", + "add_alias_error": "Erro ao adicionar apelido: {error}", + "move_account": "Mover conta", + "actor_type": "Essa conta é:", + "actor_type_description": "Marcando a sua conta como um grupo irá fazer com que ela automaticamente repita os status que a mencionam.", + "actor_type_Person": "um usuário normal", + "actor_type_Service": "um bot", + "actor_type_Group": "um grupo", + "account_backup": "Backup da conta", + "confirm_dialogs_unfollow": "deixando de seguir usuário", + "confirm_dialogs_block": "bloqueando um usuário", + "confirm_dialogs_remove_follower": "removendo um seguidor", + "remove_alias": "Remover esse apelido", + "new_alias_target": "Adicionar um novo apelido (e.g. {example})", + "added_alias": "Apelido adicionado.", + "move_account_target": "Conta de destino (e.g. {example})", + "moved_account": "Conta movida.", + "remove_language": "Remover", + "primary_language": "Linguagem primária:", + "fallback_language": "Linguagem de reserva {index}:", + "add_language": "Adicionar linguagem de reserva", + "expert_mode": "Mostrar avançados", + "setting_changed": "As configurações são diferentes do padrão", + "setting_server_side": "Essas configurações estão atreladas ao seu perfil e afetarão todas as sessões e clientes", + "mention_links": "Links de menção", + "confirm_dialogs_mute": "mutando um usuário", + "backup_not_ready": "Esse backup não está pronto ainda.", + "remove_backup": "Remover" }, "timeline": { "collapse": "Esconder", @@ -699,7 +807,20 @@ "load_all": "A carregar todos os {emojiAmount} emojis", "load_all_hint": "Carregado o primeiro emoji {saneAmount}, carregar todos os emojis pode causar problemas de desempenho.", "keep_open": "Manter o seletor aberto", - "stickers": "Autocolantes" + "stickers": "Autocolantes", + "hide_custom_emoji": "Ocultar emojis customizados", + "unicode_groups": { + "symbols": "Símbolos", + "activities": "Atividades", + "animals-and-nature": "Animais & Natureza", + "people-and-body": "Pessoas & Corpo", + "smileys-and-emotion": "Sorriso & Emoção", + "travel-and-places": "Viagem & Lugares", + "food-and-drink": "Comida & Bebidas", + "objects": "Objetos" + }, + "regional_indicator": "Indicador regional {letter}", + "unpacked": "Emoji desempacotado" }, "polls": { "single_choice": "Escolha única", @@ -713,7 +834,9 @@ "expiry": "Tempo para finalizar sondagem", "multiple_choices": "Escolha múltipla", "type": "Tipo de sondagem", - "add_poll": "Adicionar Sondagem" + "add_poll": "Adicionar Sondagem", + "votes_count": "{count} voto | {count} votos", + "people_voted_count": "{count} pessoa votou | {count} pessoas votaram" }, "importer": { "error": "Ocorreu um erro ao importar este ficheiro.", @@ -737,7 +860,9 @@ "load_older": "Carregar interações mais antigas", "follows": "Novos seguidores", "favs_repeats": "Gostos e Partilhas", - "moves": "O utilizador migra" + "moves": "O utilizador migra", + "emoji_reactions": "Reações de Emoji", + "reports": "Relatórios" }, "errors": { "storage_unavailable": "O Pleroma não conseguiu aceder ao armazenamento do navegador. A sua sessão ou definições locais não serão armazenadas e poderá encontrar problemas inesperados. Tente ativar as cookies." @@ -828,5 +953,35 @@ "day_short": "{0}d", "days": "{0} dias", "day": "{0} dia" + }, + "report": { + "state_closed": "Fechar", + "reported_statuses": "Estado das denúncias:", + "reported_user": "Usuário denunciado:", + "state_resolved": "Resolvido", + "state": "Estado:", + "state_open": "Abrir", + "notes": "Notas:" + }, + "announcements": { + "start_time_display": "Inicia às {time}", + "post_form_header": "Enviar anúncio", + "post_placeholder": "Digite o conteúdo do seu anúncio aqui...", + "page_header": "Anúncios", + "title": "Anúncio", + "mark_as_read_action": "Marcar como lido", + "post_action": "Postar", + "post_error": "Erro: {error}", + "close_error": "Fechar", + "delete_action": "Apagar", + "start_time_prompt": "Tempo de início: ", + "end_time_prompt": "Tempo de término: ", + "all_day_prompt": "Esse é um evento para o dia todo", + "published_time_display": "Publicado às {time}", + "end_time_display": "Finaliza às {time}", + "edit_action": "Editar", + "submit_edit_action": "Enviar", + "cancel_edit_action": "Cancelar", + "inactive_message": "Esse anúncio está inativo" } } diff --git a/src/i18n/uk.json b/src/i18n/uk.json @@ -33,11 +33,11 @@ "public": "Публічне", "unlisted": "Непублічне" }, - "undo": "Відмінити", + "undo": "Скасувати", "yes": "Так", "no": "Ні", "unpin": "Відкріпити", - "scroll_to_top": "Вгору", + "scroll_to_top": "Піднятися вгору", "pin": "Прикріпити" }, "finder": { @@ -88,7 +88,7 @@ "simple_policies": "Правила поточного інстансу", "reason": "Причина", "not_applicable": "н/в", - "instance": "Інстанс" + "instance": "Сервер" }, "mrf_policies_desc": "Правила MRF розповсюджуються на даний інстанс. Наступні правила активні:", "mrf_policies": "Активувати правила MRF (модуль переписування повідомлень)", @@ -121,7 +121,8 @@ "placeholder": "напр. stepan", "logout_confirm": "Ви дійсно хочете вийти?", "logout_confirm_accept_button": "Вийти", - "logout_confirm_cancel_button": "Ні, хочу назад!" + "logout_confirm_cancel_button": "Ні, хочу назад!", + "logout_confirm_title": "Вихід" }, "importer": { "error": "Під час імпортування файлу сталася помилка.", @@ -164,7 +165,13 @@ "broken_favorite": "Невідомий допис, шукаю його…", "error": "Помилка при оновленні сповіщень: {0}", "poll_ended": "опитування закінчено", - "submitted_report": "подав скаргу" + "submitted_report": "подав скаргу", + "unread_announcements": "{num} непрочитане оголошення | {num} непрочитаних оголошень", + "unread_chats": "{num} непрочитаний чат | {num} непрочитаних чатів", + "unread_follow_requests": "{num} новий запит на підписку | {num} нових запитів на підписку", + "configuration_tip": "Ви можете налаштувати, що відображати тут у {theSettings}. {dismiss}", + "configuration_tip_settings": "налаштування", + "configuration_tip_dismiss": "Не показувати знову" }, "nav": { "chats": "Чати", @@ -193,7 +200,9 @@ "mobile_notifications_close": "Закрити сповіщення", "edit_nav_mobile": "Редагувати панель навігації", "announcements": "Анонси", - "search_close": "Закрити панель пошуку" + "search_close": "Закрити панель пошуку", + "mobile_notifications_mark_as_seen": "Позначити все прочитаним", + "quotes": "Цитування" }, "media_modal": { "next": "Наступна", @@ -267,7 +276,9 @@ "activities": "Активності", "symbols": "Символи", "travel-and-places": "Подорожі та Місця" - } + }, + "unpacked": "Розпаковані емоджі", + "hide_custom_emoji": "Приховати кастомні емодзі" }, "post_status": { "content_type": { @@ -282,7 +293,7 @@ "new_status": "Створити допис", "direct_warning_to_first_only": "Цей допис побачать лише користувачі, що були згадані на початку повідомлення.", "direct_warning_to_all": "Цей допис побачать всі згадані користувачі.", - "default": "Що нового?", + "default": "Щойно приземлились у Борисполі.", "content_warning": "Тема (необов'язково)", "preview": "Попередній перегляд", "posting": "Відправляється", @@ -304,7 +315,11 @@ "post": "Опублікувати", "edit_unsupported_warning": "Pleroma не підтримує редагування згадувань чи голосувань.", "edit_status": "Редагувати допис", - "edit_remote_warning": "Інші віддалені інстанси можуть не підтримувати редагування та вони можуть не отримати актуальну версію допису." + "edit_remote_warning": "Інші віддалені інстанси можуть не підтримувати редагування та вони можуть не отримати актуальну версію допису.", + "content_type_selection": "Форматування допису", + "scope_notice_dismiss": "Закрити це сповіщення", + "reply_option": "Відповісти на цей допис", + "quote_option": "Процитувати допис" }, "settings": { "blocks_imported": "Блокування імпортовані! Їх обробка триватиме певний час.", @@ -529,7 +544,7 @@ "header": "Попередній перегляд", "link": "невеличке посилання", "header_faint": "Це нормально", - "input": "Що нового?", + "input": "Щойно приземлився у Борисполі.", "checkbox": "Я переглянув умови використання", "fine_print": "Прочитайте наш {0} аби нічого нового не дізнатись!", "faint_link": "корисний підручник" @@ -730,12 +745,12 @@ "conversation_display_tree_quick": "Вигляд дерева", "disable_sticky_headers": "Не закріплювати заголовок колонки зверху на сторінці", "third_column_mode_none": "Не показувати третю колонку взагалі", - "third_column_mode_notifications": "Колонка сповіщень", + "third_column_mode_notifications": "Колонку сповіщень", "columns": "Колонки", "auto_update": "Автоматично показувати нові дописи", "use_websockets": "Використовувати вебсокети (Оновлення в реальному часі)", "use_at_icon": "Показувати {'@'} символ як іконку замість тексту", - "mute_bot_posts": "Приховати дописи ботів", + "mute_bot_posts": "Приховувати дописи ботів", "always_show_post_button": "Завжди показувати плаваючу кнопку «Новий Допис»", "hide_favorites_description": "Не показувати список моїх вподобань (люди все одно отримують сповіщення)", "third_column_mode": "Коли достатньо місця, показувати третю колонку, що містить", @@ -743,7 +758,100 @@ "wordfilter": "Фільтр слів", "mention_links": "Посилання для згадування", "user_profiles": "Профілі користувачів", - "notification_visibility_polls": "Закінчення опитувань, в яких ви проголосували" + "notification_visibility_polls": "Закінчення опитувань, в яких ви проголосували", + "remove_language": "Вилучити", + "primary_language": "Основна мова:", + "fallback_language": "Резервна мова {index}:", + "confirm_dialogs_deny_follow": "тим, як відмовити у запиті на підписку", + "confirm_dialogs_remove_follower": "видаленням підписника", + "notification_show_extra": "Показувати додаткові сповіщення в панелі сповіщень", + "notification_extra_chats": "Показувати непрочитані чати", + "notification_extra_announcements": "Показувати непрочитані оголошення", + "notification_extra_follow_requests": "Показувати нові запити на підписку", + "third_column_mode_postform": "Форму відправки повідомлень та панель навігації", + "notification_extra_tip": "Показати пораду з налаштувань для додаткових сповіщень", + "backup_running": "Резервне копіювання триває, оброблено {number} записи. | Резервне копіювання триває, оброблено {number} записів.", + "backup_failed": "Резервне копіювання не вдалося.", + "preview": "Попередній перегляд", + "url": "URL", + "birthday": { + "label": "День народження", + "show_birthday": "Показувати мій день народження" + }, + "confirm_dialogs": "Запитувати підтвердження перед", + "confirm_dialogs_repeat": "поширенням допису", + "confirm_dialogs_unfollow": "скасуванням підписки", + "confirm_dialogs_block": "блокуванням користувача", + "confirm_dialogs_mute": "тим, як заглушити користувача", + "show_scrollbars": "Показувати смугу прокрутки на бічних панелях", + "column_sizes": "Розміри панелей", + "column_sizes_sidebar": "Бічна панель", + "add_language": "Додати резервну мову", + "confirm_dialogs_delete": "видаленням допису", + "confirm_dialogs_logout": "виходом із системи", + "confirm_dialogs_approve_follow": "схваленням запиту на підписку", + "mute_sensitive_posts": "Не стежити за чутливими постами", + "notification_visibility_follow_requests": "Запити на стеження", + "notification_visibility_reports": "Скарги", + "conversation_display_linear": "Линійний стиль", + "conversation_display_linear_quick": "Линійний вигляд", + "conversation_other_replies_button": "Показувати кнопку \"інші відповіді\"", + "conversation_other_replies_button_below": "Нижче статусів", + "mention_link_bolden_you": "Підсвічувати згадки в яких вас згадано", + "notification_setting_ignore_inactionable_seen_tip": "Це насправді не позначить ці сповіщення прочитанними, і ви все одно отримаєте сповіщення на робочому столі", + "notification_setting_unseen_at_top": "Показувати непрочитані сповіщення згори", + "mention_link_show_avatar": "Показувати світлину користувача поруч з посиланням", + "column_sizes_notifs": "Сповіщення", + "commit_value": "Зберегти", + "commit_value_tooltip": "Значення не збережено, натисніть цю кнопку щоб зберегти зміни", + "units": { + "time": { + "m": "хвилин", + "s": "секунд", + "h": "годин", + "d": "днів" + } + }, + "hide_scrobbles_after": "Приховати прослуховування старіші чим", + "conversation_other_replies_button_inside": "Всередині статусів", + "mention_link_display": "Показувати посилання на згадки", + "user_popover_avatar_action": "Дія при натисканні на світлину", + "notification_setting_ignore_inactionable_seen": "Ігнорувати прочитаний статус сповіщень, на які неможливо відреагувати (вподобання, репости і тд)", + "user_popover_avatar_action_close": "Закрити панель", + "reset_value": "Скинути", + "enable_web_push_always_show_tip": "Деякі браузери (Chromium, Chrome) потребують щоб push повідомлення завжди були сповіщенням, інакше ви побачите загальне повідомлення \"Сайт було оновлено у фоні\". Увімкніть це налаштування щоб запобігти цьому повідомленню. Може призвести до подвійних сповіщень у інших браузерах.", + "autocomplete_select_first": "Автоматично обирати перше значення коли доступні результати автозаповнення", + "hide_scrobbles": "Приховати прослуховування", + "notification_visibility_in_column": "Показувати в панелі сповіщень", + "tree_advanced": "Дозволити більш гнучку навігацію при розгорнутому перегляді", + "tree_fade_ancestors": "Показувати похідні статуси більш блідим текстом", + "notification_setting_drawer_marks_as_seen": "Закриття панелі в мобільній версії позначає всі сповіщення прочитанними", + "user_popover_avatar_overlay": "Показувати картку користувача над світлиною", + "show_yous": "Показати (Вас)", + "notification_setting_annoyance": "Роздратування", + "notification_setting_filters_chrome_push": "У деяких браузерах (Google Chrome) може бути неможливо повністю відфільтрувати сповіщення за типом, коли вони надходять через Push", + "enable_web_push_always_show": "Завжди показувати web push сповіщення", + "user_popover_avatar_action_zoom": "Збільшити світлину", + "actor_type_description": "Позначення вашого акаунту як групового змусить його автоматично повторювати статуси, які вас згадують.", + "actor_type_Person": "звичайний користувач", + "actor_type_Service": "бот", + "actor_type_Group": "група", + "actor_type": "Цей акаунт:", + "notification_visibility_native_notifications": "Показувати нативне сповіщення", + "column_sizes_content": "Зміст", + "mention_link_display_full": "завжди повні імена (наприклад {'@'}petro{'@'}poroshenko.org)", + "force_theme_recompilation_debug": "Вимкнути кеш теми, увімкнути перекомпіляцію при кожному старті (ВІДЛАДКА)", + "mention_link_use_tooltip": "Показувати картку користувача при натисканні згадки", + "mention_link_show_avatar_quick": "Показувати світлину користувача поруч зі згадками", + "mention_link_fade_domain": "Скорочувати домени (наприклад {'@'}poroshenko.org в {'@'}petro{'@'}poroshenko.org)", + "hard_reset_value": "Скинути всі налаштування", + "reset_value_tooltip": "Відкинути чернетку", + "hard_reset_value_tooltip": "Прибрати налаштування зі сховища, буде використовуватись значення за замовчуванням", + "emoji_reactions_scale": "Масштабування реакцій", + "max_depth_in_thread": "Максимальна кількість рівнів треду для відображення за замовчуванням", + "mention_link_display_full_for_remote": "як повні імена тільки для користувачів з інших серверів (наприклад {'@'}petro{'@'}poroshenko.org)", + "mention_link_display_short": "завжди як короткі імена (наприклад {'@'}petro)", + "hide_actor_type_indication": "Приховати позначення типу акаунту (бот, група і тд) в постах" }, "selectable_list": { "select_all": "Вибрати все" @@ -760,7 +868,9 @@ "password_required": "не може бути порожнім", "email_required": "не може бути порожнім", "fullname_required": "не може бути порожнім", - "username_required": "не може бути порожнім" + "username_required": "не може бути порожнім", + "birthday_required": "не може бути пустим", + "birthday_min_age": "має бути в або перед {date}" }, "bio_placeholder": "напр.\nНаш народ завжди прагне волі для себе і бажає її для інших народів. Він боровся і бореться за правду і справедливість. Ми хочемо жити у згоді і взаємному шануванні з усіми народами доброї волі. Такі самі права визнаємо за іншими народами, за які боремося для себе.", "fullname_placeholder": "напр. Степан Бандера", @@ -778,7 +888,9 @@ "reason": "Причина реєстрації", "bio_optional": "Біографія (необов'язково)", "email_language": "Якою мовою ви бажаєте отримувати електронні листи від сервера?", - "email_optional": "Ел. пошта (необов'язково)" + "email_optional": "Ел. пошта (необов'язково)", + "birthday": "День народження:", + "birthday_optional": "День народження (необов'язково):" }, "who_to_follow": { "who_to_follow": "На кого підписатися", @@ -793,7 +905,10 @@ "reject_follow_request": "Відхилити запит на підписку", "accept_follow_request": "Прийняти запит на підписку", "media_upload": "Завантажити медіа", - "bookmark": "Додати до закладок" + "bookmark": "Додати до закладок", + "toggle_expand": "Розгорнути або згорнути сповіщення щоб показати допис повністю", + "toggle_mute": "Розгорнути або згорнути сповіщення щоб відкрити заглушений контент", + "autocomplete_available": "{number} результат. Використовуйте клавіши зі стрілками для навігації. | {number} результатів доступно. Використовуйте клавіши зі стрілками для навігації." }, "upload": { "error": { @@ -865,7 +980,9 @@ "hashtags": "Хештеги", "people": "Люди", "people_talking": "{count} людей говорять про це", - "person_talking": "{count} особа говорить про це" + "person_talking": "{count} особа говорить про це", + "no_more_results": "Більше немає", + "load_more": "Завантажити ще" }, "user_card": { "statuses": "Дописи", @@ -890,7 +1007,8 @@ "grant_moderator": "Надати права модератора", "revoke_admin": "Позбавити прав адміністратора", "grant_admin": "Надати права адміністратора", - "quarantine": "Не розповсюджувати дописи на інших інстансах" + "quarantine": "Не розповсюджувати дописи на інших інстансах", + "delete_user_data_and_deactivate_confirmation": "Це назовсім видалить дані обліковки й вимкне її. Точно продовжити?" }, "deny": "Відмовити", "block": "Заблокувати", @@ -929,7 +1047,40 @@ "bot": "Бот", "edit_profile": "Редагувати профіль", "deactivated": "Деактивований", - "follow_cancel": "Скасувати запит" + "follow_cancel": "Скасувати запит", + "block_confirm_title": "Блокування", + "block_confirm": "Точно заблокувати {user}?", + "mute_confirm_cancel_button": "Ні, не приглушувати", + "note_blank": "(Пусто)", + "edit_note_apply": "Застосувати", + "edit_note_cancel": "Скасувати", + "block_confirm_accept_button": "Так, заблокувати", + "block_confirm_cancel_button": "Ні, не блокувати", + "deny_confirm_title": "Відхилити запит на підписку", + "mute_confirm_accept_button": "Так, приглушити", + "mute_confirm": "Точно приглушити {user}?", + "edit_note": "Редагувати нотатку", + "mute_confirm_title": "Приглушення", + "mute_duration_prompt": "Приглушити користувача на (0 якщо назавжди):", + "approve_confirm_title": "Дозвіл підписатись", + "approve_confirm_accept_button": "Так, дозволити", + "approve_confirm_cancel_button": "Ні, скасувати", + "deny_confirm_accept_button": "Так, відхилити", + "deny_confirm_cancel_button": "Ні, скасувати", + "deny_confirm": "Ви точно хочете відхилити запит на підписку від {user}?", + "unfollow_confirm_title": "Відписка", + "unfollow_confirm": "Точно відписатись від {user}?", + "unfollow_confirm_accept_button": "Так, відписатись", + "unfollow_confirm_cancel_button": "Ні, не відписуватись", + "note": "Приватна нотатка", + "group": "Група", + "remove_follower_confirm": "Ви дійсно хочете прибрати користувача {user} з ваших фоловерів?", + "remove_follower_confirm_title": "Підтверджувати відписку", + "remove_follower": "Відписка", + "remove_follower_confirm_accept_button": "Прибрати", + "remove_follower_confirm_cancel_button": "Зберегти", + "birthday": "День народження: {birthday}", + "approve_confirm": "Прийняти запит на стеження від {user}?" }, "status": { "copy_link": "Скопіювати посилання на допис", @@ -965,7 +1116,44 @@ "plus_more": "+{number} більше", "thread_show_full_with_icon": "{icon} {text}", "show_only_conversation_under_this": "Показати всі відповіді на цей допис", - "status_history": "Історія змін" + "status_history": "Історія змін", + "thread_hide": "Сховати гілку", + "open_gallery": "Відкрити галерею", + "repeat_confirm": "Точно поширити допис?", + "repeat_confirm_title": "Підтвердьте поширення", + "repeat_confirm_accept_button": "Так, поширити", + "repeat_confirm_cancel_button": "Ні, не поширювати", + "delete_error": "Помилка при видаленні допису: {0}", + "delete_confirm_accept_button": "Так, видалити", + "delete_confirm_cancel_button": "Ні, лишити", + "delete_confirm_title": "Підтвердьте видалення", + "you": "(ви)", + "collapse_attachments": "Згорнути вкладення", + "show_all_attachments": "Показати всі вкладення", + "hide_attachment": "Сховати вкладення", + "many_attachments": "Вкладень: {number} | Вкладень: {number}", + "attachment_stop_flash": "Зупинити Flash-плеєр", + "thread_follow": "Ще відповідей: {numStatus} | Ще відповідей: {numStatus}", + "remove_attachment": "Видалити вкладення", + "ancestor_follow": "Переглянути ще {numReplies} під цим дописом | Переглянути ще {numReplies} під цим дописом", + "show_all_conversation": "Показати всю розмову (ще дописів: {numStatus}) | Показати всю розмову (ще дописів: {numStatus})", + "move_up": "Посунути вкладення ліворуч", + "move_down": "Посунути вкладення праворуч", + "thread_show": "Показати гілку", + "mentions": "Згадки", + "thread_show_full": "Показати відповіді: ({numStatus}/{depth}) | Показати відповіді: ({numStatus}/{depth})", + "hide_quote": "Сховати процитований допис", + "display_quote": "Показати процитований допис", + "invisible_quote": "Процитований допис недоступний: {link}", + "replies_list_with_others": "Ще відповідей: {numReplies} | Ще відповідей: {numReplies}:", + "show_attachment_in_modal": "Показати вкладення у вікні", + "show_attachment_description": "Переглянути опис (натисніть саме вкладення, якщо опис не вміщається)", + "quotes": "Цитування", + "load_error": "Неможливо завантажити статус: {error}", + "loading": "Завантаження...", + "sensitive_muted": "Заглушення чутливого контенту", + "reaction_count_label": "{num} людина відреагувала | {num} людей відреагували", + "more_actions": "Більше дій для цього статусу" }, "timeline": { "no_more_statuses": "Більше немає дописів", @@ -980,7 +1168,9 @@ "repeated": "поширив(-ла)", "no_retweet_hint": "Запис, позначено як \"тільки для підписників\" або \"особисте\" і тому не може бути поширений", "socket_broke": "Втрачено з'єднання у реальному часі: код {0}", - "socket_reconnected": "Встановлено з'єднання у реальному часі" + "socket_reconnected": "Встановлено з'єднання у реальному часі", + "quick_view_settings": "Налаштування швидкого перегляду", + "quick_filter_settings": "Налаштування швидкого фільтру" }, "user_reporting": { "submit": "Відправити", @@ -1026,5 +1216,171 @@ "submit_edit_action": "Надіслати", "cancel_edit_action": "Скасувати", "inactive_message": "Це оголошення неактивне" + }, + "lists": { + "really_delete": "Дійсно видалити список?", + "error": "Помилка при роботі зі списками: {0}", + "is_in_list": "Вже є у списку", + "editing_list": "Редагування списку {listTitle}", + "creating_list": "Створення нового списку", + "search": "Знайти користувачів", + "create": "Створити", + "save": "Зберегти зміни", + "manage_members": "Керувати учасниками списку", + "new": "Новий список", + "title": "Назва списку", + "delete": "Видалити список", + "following_only": "Лише за ким ви стежите", + "lists": "Списки", + "manage_lists": "Керувати списками", + "remove_from_list": "Видалити зі списку", + "add_to_list": "Додати до списку", + "update_title": "Зберегти назву", + "add_members": "Шукати більше користувачів" + }, + "update": { + "update_changelog": "Щоб дізнатись більше інформації, дивіться {theFullChangelog}.", + "update_bugs": "Будь ласка, повідомляйте про будь-які проблеми та помилки на {pleromaGitlab}, оскільки ми внесли багато змін, і навіть після ретельно проведених перевірок, ми можемо щось пропустити. Ми заздалегідь вдячні за ваші відгуки щодо проблем, з якими ви можете зіткнутися, а також пропозиції щодо вдосконалення Pleroma та Pleroma-FE.", + "update_changelog_here": "повний список змін", + "big_update_title": "Хвилинку уваги", + "update_bugs_gitlab": "Pleroma GitLab", + "big_update_content": "У нас не було оновлень протягом тривалого часу, тому речі можуть мати інакший вигляд, аніж ви звикли.", + "art_by": "Арт від {linkToArtist}" + }, + "unicode_domain_indicator": { + "tooltip": "Цей домен містить не-ASCII символи." + }, + "admin_dash": { + "window_title": "Адміністрування", + "tabs": { + "instance": "Сервер (Instance)", + "frontends": "Фронтенди", + "nodb": "Немає конфігурації бази даних", + "emoji": "Емодзі", + "limits": "Ліміти" + }, + "nodb": { + "heading": "Конфіг бази даних вимкнено", + "text": "Вам потрібно змінити налаштування бекенду таким чином, щоб {property} дорівнювало {value}, детальніше у {documentation}.", + "text2": "Більшість налаштувань будуть недоступні.", + "documentation": "документація" + }, + "frontend": { + "install": "Встановити", + "install_version": "Встановити версію {version}", + "success_installing_frontend": "Фронтенд версії {version} успішно встановлено", + "failure_installing_frontend": "Не вдалось встановити версію {version}: {reason}", + "repository": "Посилання на репозиторій", + "versions": "Доступні версії", + "is_default_custom": "(За замовчуванням, версія: {version})", + "build_url": "URL збірки", + "reinstall": "Перевстановити", + "default_frontend_unavail": "Налаштування фронтенду недоступні, адже вони потребують конфігурації бази даних", + "default_frontend_tip": "Фронтенд за замовчуванням будуть бачити всі користувачі. На сьогоднішній день немає можливості обирати персональний фронтенд під кожного користувача. Якщо ви не користуватиметесь PleromaFE, то, скоріш за все, вам доведеться користуватись старим та забагованим AdminFE для налаштування свого серверу, допоки ми не придумаємо нічого кращого.", + "set_default": "Призначити за замовчуванням", + "set_default_version": "Призначити версію {version} за замовчуванням", + "wip_notice": "Будь ласка майте на увазі що цей розділ знаходиться у процесі розробки та певні функції можуть не працювати.", + "default_frontend": "Фронтенд за замовчуванням", + "available_frontends": "Доступно для встановлення", + "is_default": "(За замовчуванням)", + "more_install_options": "Більше варіантів встановлення", + "more_default_options": "Більше налаштувань за замовчуванням" + }, + "emoji": { + "adding_new": "Додати новий емодзі", + "shortcode": "Шорткод", + "filename": "Назва файлу", + "add_file": "Додати файл", + "importFS": "Імпортувати емодзі з файлової системи", + "global_actions": "Глобальні дії", + "reload": "Перезавантажити емодзі", + "error": "Помилка: {0}", + "delete_pack": "Видалити набір", + "create_pack": "Створити набір", + "create": "Створити", + "new_pack_name": "Нова назва набору", + "emoji_packs": "Набори емодзі", + "remote_packs": "Віддалені набори", + "do_list": "Список", + "remote_pack_instance": "Сервер з віддаленими наборами", + "homepage": "Домашня сторінка", + "edit_pack": "Редагувати набір", + "description": "Опис", + "fallback_src": "Джерело заміни", + "share": "Поділитись", + "fallback_sha256": "Заміна SHA256", + "delete_confirm": "Ви впевнені, що хочете видалити {0}?", + "download_pack": "Завантажити набір", + "downloading_pack": "Завантаження {0}", + "download": "Завантажити", + "new_filename": "Назва файлу, залиште порожнім для автозаповнення", + "download_as_name": "Нове ім'я", + "editing": "Редагування {0}", + "delete_title": "Видалити?", + "download_as_name_full": "Нове ім'я, залиште порожнім для перевикористання", + "files": "Файли", + "metadata_changed": "Метадані відрізняються від збережених", + "replace_warning": "Це ЗАМІНИТЬ локальний набір з такою самою назвою", + "emoji_changed": "Незбережені зміни файлу емодзі, перевірте підсвічений емодзі", + "emoji_pack": "Набір емодзі", + "revert_meta": "Відновити метадані", + "save": "Зберегти", + "delete": "Видалити", + "revert": "Відновити", + "save_meta": "Зберегти метадані", + "new_shortcode": "Шорткод, залиште порожнім для автозаповнення" + }, + "instance": { + "restrict": { + "activities": "Доступ до статусів/активностей", + "header": "Обмежити доступ для анонімних відвідувачів", + "timelines": "Доступ до стрічок", + "profiles": "Доступ до профілів користувачів", + "description": "Детальне налаштування для контролю доступу до певних розділів API. За замовчуванням (невизначений стан) доступ буде заборонений якщо сервер не публічний, увімкнене налаштування забороняє доступ навіть до публічного серверу, вимкнене налаштування дозволяє доступ навіть до приватного серверу. Неправильні налаштування можуть призвести до небажаних наслідків: наприклад, якщо доступ до профілю обмежений, то пости будуть відображатись без інформації про профіль." + }, + "registrations": "Заявки на реєстрацію", + "instance": "Інформація про сервер", + "access": "Доступ до серверу", + "captcha_header": "CAPTCHA", + "kocaptcha": "налаштування KoCaptcha" + }, + "reset_all": "Скинути все", + "commit_all": "Зберегти все", + "captcha": { + "kocaptcha": "KoCaptchа", + "native": "Нативний" + }, + "limits": { + "uploads": "Ліміти вкладень", + "users": "Ліміти користувацьких профілей", + "profile_fields": "Ліміти полів у профілі", + "arbitrary_limits": "Довільні ліміти", + "user_uploads": "Ліміти медіа у профілі", + "posts": "Ліміти дописів" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":limit_to_local_content": { + "label": "Обмежити пошуки локальним контентом", + "description": "Вимикає глобальних пошук по мережі для неавторизованих (за замовчуванням), всіх користувачів або нікого" + }, + ":description_limit": { + "description": "Максимальна довжина поля опису вкладень", + "label": "Обмеження" + }, + ":public": { + "description": "Вимкнення цього зробить API доступним тільки залогіненим користувачам, таким чином Публічна стрічка та стрічка Федерації будуть недоступні неавторизованим користувачам.", + "label": "Публічний сервер" + }, + ":background_image": { + "label": "Тло", + "description": "Тло (використовується PleromaFE)" + } + } + } + }, + "wip_notice": "Ця адмінська панель експериментальна, {adminFeLink}.", + "old_ui_link": "старий інтерфейс адмінки доступний тут" } } diff --git a/src/i18n/zh.json b/src/i18n/zh.json @@ -146,7 +146,13 @@ "follow_request": "想要关注你", "error": "取得通知时发生错误:{0}", "poll_ended": "投票结束了", - "submitted_report": "提交举报" + "submitted_report": "提交举报", + "unread_announcements": "{num} 条未读公告", + "unread_chats": "{num} 条未读聊天讯息", + "unread_follow_requests": "{num} 个新关注请求", + "configuration_tip": "可以在 {theSettings} 里定制什么会显示在这里。{dismiss}", + "configuration_tip_settings": "设置", + "configuration_tip_dismiss": "不再显示" }, "polls": { "add_poll": "增加投票", @@ -212,7 +218,9 @@ "edit_unsupported_warning": "Pleroma 不支持对提及或投票进行编辑。", "edit_status": "编辑状态", "content_type_selection": "发帖格式", - "scope_notice_dismiss": "关闭此提示" + "scope_notice_dismiss": "关闭此提示", + "reply_option": "回复这条状态", + "quote_option": "引用这条状态" }, "registration": { "bio": "简介", @@ -747,7 +755,12 @@ "reset_value_tooltip": "重置草稿", "hard_reset_value": "硬重置", "hard_reset_value_tooltip": "从存储中移除设置,强制使用默认值", - "emoji_reactions_scale": "表情回应比例系数" + "emoji_reactions_scale": "表情回应比例系数", + "notification_show_extra": "在通知栏里显示额外通知", + "notification_extra_chats": "显示未读聊天", + "notification_extra_announcements": "显示未读公告", + "notification_extra_follow_requests": "显示新的关注请求", + "notification_extra_tip": "显示额外通知的定制提示" }, "time": { "day": "{0} 天", @@ -880,7 +893,10 @@ "show_attachment_in_modal": "在媒体模式中显示", "status_history": "状态历史", "delete_error": "删除状态时出错:{0}", - "reaction_count_label": "{num} 人作出了表情回应" + "reaction_count_label": "{num} 人作出了表情回应", + "invisible_quote": "引用的状态不可用:{link}", + "hide_quote": "隐藏引用的状态", + "display_quote": "显示引用的状态" }, "user_card": { "approve": "核准", @@ -927,7 +943,7 @@ "sandbox": "强制帖子为只有关注者可看", "disable_remote_subscription": "禁止从远程实例关注用户", "disable_any_subscription": "完全禁止关注用户", - "quarantine": "从联合实例中禁止用户帖子", + "quarantine": "不许帖子传入别站", "delete_user": "删除用户", "delete_user_data_and_deactivate_confirmation": "这将永久删除该账户的数据并停用该账户。你完全确定吗?" }, @@ -1184,7 +1200,7 @@ "big_update_title": "请忍耐一下", "big_update_content": "我们已经有一段时间没有发布发行版,所以事情的外观和感觉可能与你习惯的不一样。", "update_bugs": "请在 {pleromaGitlab} 上报告任何问题和bug,因为我们已经改变了很多,虽然我们进行了彻底的测试,并且自己使用了开发版本,但我们可能错过了一些东西。我们欢迎你对你可能遇到的问题或如何改进Pleroma和Pleroma-FE提出反馈和建议。", - "art_by": "Art by {linkToArtist}" + "art_by": "{linkToArtist} 的作品" }, "lists": { "search": "搜索用户", diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js @@ -38,7 +38,7 @@ export default function createPersistedState ({ }, setState = (key, state, storage) => { if (!loaded) { - console.log('waiting for old state to be loaded...') + console.info('waiting for old state to be loaded...') return Promise.resolve() } else { return storage.setItem(key, state) @@ -65,7 +65,7 @@ export default function createPersistedState ({ } loaded = true } catch (e) { - console.log("Couldn't load state") + console.error("Couldn't load state") console.error(e) loaded = true } @@ -86,8 +86,8 @@ export default function createPersistedState ({ }) } } catch (e) { - console.log("Couldn't persist state:") - console.log(e) + console.error("Couldn't persist state:") + console.error(e) } }) } diff --git a/src/main.js b/src/main.js @@ -6,6 +6,7 @@ import './lib/event_target_polyfill.js' import interfaceModule from './modules/interface.js' import instanceModule from './modules/instance.js' import statusesModule from './modules/statuses.js' +import notificationsModule from './modules/notifications.js' import listsModule from './modules/lists.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' @@ -78,6 +79,7 @@ const persistedStateOptions = { // TODO refactor users/statuses modules, they depend on each other users: usersModule, statuses: statusesModule, + notifications: notificationsModule, lists: listsModule, api: apiModule, config: configModule, diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js @@ -105,7 +105,6 @@ const adminSettingsStorage = { } set(config, path, convert(c.value)) }) - console.log(config[':pleroma']) commit('updateAdminSettings', { config, modifiedPaths }) commit('resetAdminDraft') }, @@ -123,7 +122,6 @@ const adminSettingsStorage = { const descriptions = {} backendDescriptions.forEach(d => convert(d, '', descriptions)) - console.log(descriptions[':pleroma']['Pleroma.Captcha']) commit('updateAdminDescriptions', { descriptions }) }, diff --git a/src/modules/api.js b/src/modules/api.js @@ -202,12 +202,13 @@ const api = { timeline = 'friends', tag = false, userId = false, - listId = false + listId = false, + statusId = false }) { if (store.state.fetchers[timeline]) return const fetcher = store.state.backendInteractor.startFetchingTimeline({ - timeline, store, userId, listId, tag + timeline, store, userId, listId, statusId, tag }) store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, diff --git a/src/modules/config.js b/src/modules/config.js @@ -1,10 +1,21 @@ import Cookies from 'js-cookie' -import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js' +import { applyConfig } from '../services/style_setter/style_setter.js' import messages from '../i18n/messages' import { set } from 'lodash' import localeService from '../services/locale/locale.service.js' const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' +const APPEARANCE_SETTINGS_KEYS = new Set([ + 'sidebarColumnWidth', + 'contentColumnWidth', + 'notifsColumnWidth', + 'textSize', + 'navbarSize', + 'panelHeaderSize', + 'forcedRoundness', + 'emojiSize', + 'emojiReactionsScale' +]) const browserLocale = (window.navigator.language || 'en').split('-')[0] @@ -24,10 +35,30 @@ export const multiChoiceProperties = [ export const defaultState = { expertLevel: 0, // used to track which settings to show and hide - colors: {}, - theme: undefined, - customTheme: undefined, - customThemeSource: undefined, + + // Theme stuff + theme: undefined, // Very old theme store, stores preset name, still in use + + // V1 + colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore + + // V2 + customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event. + customThemeSource: undefined, // "source", stores original theme data + + // V3 + themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions + forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists + theme3hacks: { // Hacks, user overrides that are independent of theme used + underlay: 'none', + fonts: { + interface: undefined, + input: undefined, + post: undefined, + monospace: undefined + } + }, + hideISP: false, hideInstanceWallpaper: false, hideShoutbox: false, @@ -36,10 +67,13 @@ export const defaultState = { hideMutedThreads: undefined, // instance default hideWordFilteredPosts: undefined, // instance default muteBotStatuses: undefined, // instance default + muteSensitiveStatuses: undefined, // instance default collapseMessageWithSubject: undefined, // instance default padEmoji: true, hideAttachments: false, hideAttachmentsInConv: false, + hideScrobbles: false, + hideScrobblesAfter: '2d', maxThumbnails: 16, hideNsfw: true, preloadImage: true, @@ -56,6 +90,7 @@ export const defaultState = { notificationVisibility: { follows: true, mentions: true, + statuses: true, likes: true, repeats: true, moves: true, @@ -65,7 +100,21 @@ export const defaultState = { chatMention: true, polls: true }, + notificationNative: { + follows: true, + mentions: true, + statuses: true, + likes: false, + repeats: false, + moves: false, + emojiReactions: false, + followRequest: true, + reports: true, + chatMention: true, + polls: true + }, webPushNotifications: false, + webPushAlwaysShowNotifications: false, muteWords: [], highlight: {}, interfaceLanguage: browserLocale, @@ -98,7 +147,12 @@ export const defaultState = { sidebarColumnWidth: '25rem', contentColumnWidth: '45rem', notifsColumnWidth: '25rem', - emojiReactionsScale: 1.0, + emojiReactionsScale: undefined, + textSize: undefined, // instance default + emojiSize: undefined, // instance default + navbarSize: undefined, // instance default + panelHeaderSize: undefined, // instance default + forcedRoundness: undefined, // instance default navbarColumnStretch: false, greentext: undefined, // instance default useAtIcon: undefined, // instance default @@ -117,8 +171,16 @@ export const defaultState = { conversationTreeAdvanced: undefined, // instance default conversationOtherRepliesButton: undefined, // instance default conversationTreeFadeAncestors: undefined, // instance default + showExtraNotifications: undefined, // instance default + showExtraNotificationsTip: undefined, // instance default + showChatsInExtraNotifications: undefined, // instance default + showAnnouncementsInExtraNotifications: undefined, // instance default + showFollowRequestsInExtraNotifications: undefined, // instance default maxDepthInThread: undefined, // instance default - autocompleteSelect: undefined // instance default + autocompleteSelect: undefined, // instance default + closingDrawerMarksAsSeen: undefined, // instance default + unseenAtTop: undefined, // instance default + ignoreInactionableSeen: undefined // instance default } // caching the instance default properties @@ -148,6 +210,10 @@ const config = { } }, mutations: { + setOptionTemporarily (state, { name, value }) { + set(state, name, value) + applyConfig(state) + }, setOption (state, { name, value }) { set(state, name, value) }, @@ -178,6 +244,37 @@ const config = { setHighlight ({ commit, dispatch }, { user, color, type }) { commit('setHighlight', { user, color, type }) }, + setOptionTemporarily ({ commit, dispatch, state, rootState }, { name, value }) { + if (rootState.interface.temporaryChangesTimeoutId !== null) { + console.warn('Can\'t track more than one temporary change') + return + } + const oldValue = state[name] + + commit('setOptionTemporarily', { name, value }) + + const confirm = () => { + dispatch('setOption', { name, value }) + commit('clearTemporaryChanges') + } + + const revert = () => { + commit('setOptionTemporarily', { name, value: oldValue }) + commit('clearTemporaryChanges') + } + + commit('setTemporaryChanges', { + timeoutId: setTimeout(revert, 10000), + confirm, + revert + }) + }, + setThemeV2 ({ commit, dispatch }, { customTheme, customThemeSource }) { + commit('setOption', { name: 'theme', value: 'custom' }) + commit('setOption', { name: 'customTheme', value: customTheme }) + commit('setOption', { name: 'customThemeSource', value: customThemeSource }) + dispatch('setTheme', { themeData: customThemeSource, recompile: true }) + }, setOption ({ commit, dispatch, state }, { name, value }) { const exceptions = new Set([ 'useStreamingApi' @@ -195,24 +292,26 @@ const config = { dispatch('disableMastoSockets') dispatch('setOption', { name: 'useStreamingApi', value: false }) }) + break } } } else { commit('setOption', { name, value }) + if (APPEARANCE_SETTINGS_KEYS.has(name)) { + applyConfig(state) + } + if (name.startsWith('theme3hacks')) { + dispatch('setTheme', { recompile: true }) + } switch (name) { case 'theme': - setPreset(value) - break - case 'sidebarColumnWidth': - case 'contentColumnWidth': - case 'notifsColumnWidth': - case 'emojiReactionsScale': - applyConfig(state) + if (value === 'custom') break + dispatch('setTheme', { themeName: value, recompile: true, saveData: true }) break - case 'customTheme': - case 'customThemeSource': - applyTheme(value) + case 'themeDebug': { + dispatch('setTheme', { recompile: true }) break + } case 'interfaceLanguage': messages.setLanguage(this.getters.i18n, value) dispatch('loadUnicodeEmojiData', value) diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -1,5 +1,3 @@ -import { getPreset, applyTheme } from '../services/style_setter/style_setter.js' -import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js' @@ -44,7 +42,7 @@ const defaultState = { registrationOpen: true, server: 'http://localhost:4040/', textlimit: 5000, - themeData: undefined, + themeData: undefined, // used for theme editor v2 vapidPublicKey: undefined, // Stuff from static/config.json @@ -71,6 +69,7 @@ const defaultState = { hideSitename: false, hideUserStats: false, muteBotStatuses: false, + muteSensitiveStatuses: false, modalOnRepeat: false, modalOnUnfollow: false, modalOnBlock: true, @@ -97,14 +96,29 @@ const defaultState = { sidebarRight: false, subjectLineBehavior: 'email', theme: 'pleroma-dark', + emojiReactionsScale: 0.5, + textSize: '14px', + emojiSize: '2.2rem', + navbarSize: '3.5rem', + panelHeaderSize: '3.2rem', + forcedRoundness: -1, + fontsOverride: {}, virtualScrolling: true, sensitiveByDefault: false, conversationDisplay: 'linear', conversationTreeAdvanced: false, conversationOtherRepliesButton: 'below', conversationTreeFadeAncestors: false, + showExtraNotifications: true, + showExtraNotificationsTip: true, + showChatsInExtraNotifications: true, + showAnnouncementsInExtraNotifications: true, + showFollowRequestsInExtraNotifications: true, maxDepthInThread: 6, autocompleteSelect: false, + closingDrawerMarksAsSeen: true, + unseenAtTop: false, + ignoreInactionableSeen: false, // Nasty stuff customEmoji: [], @@ -129,6 +143,7 @@ const defaultState = { suggestionsEnabled: false, suggestionsWeb: '', quotingAvailable: false, + groupActorAvailable: false, // Html stuff instanceSpecificPanelContent: '', @@ -269,9 +284,6 @@ const instance = { dispatch('initializeSocket') } break - case 'theme': - dispatch('setTheme', value) - break } }, async getStaticEmoji ({ commit }) { @@ -360,25 +372,6 @@ const instance = { console.warn(e) } }, - - setTheme ({ commit, rootState }, themeName) { - commit('setInstanceOption', { name: 'theme', value: themeName }) - getPreset(themeName) - .then(themeData => { - commit('setInstanceOption', { name: 'themeData', value: themeData }) - // No need to apply theme if there's user theme already - const { customTheme } = rootState.config - if (customTheme) return - - // New theme presets don't have 'theme' property, they use 'source' - const themeSource = themeData.source - if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { - applyTheme(themeSource) - } else { - applyTheme(themeData.theme) - } - }) - }, fetchEmoji ({ dispatch, state }) { if (!state.customEmojiFetched) { state.customEmojiFetched = true diff --git a/src/modules/interface.js b/src/modules/interface.js @@ -1,4 +1,13 @@ +import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' +import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' + const defaultState = { + localFonts: null, + themeApplied: false, + temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout + temporaryChangesConfirm: () => {}, // used for applying temporary options + temporaryChangesRevert: () => {}, // used for reverting temporary options settingsModalState: 'hidden', settingsModalLoadedUser: false, settingsModalLoadedAdmin: false, @@ -13,7 +22,8 @@ const defaultState = { cssFilter: window.CSS && window.CSS.supports && ( window.CSS.supports('filter', 'drop-shadow(0 0)') || window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') - ) + ), + localFonts: typeof window.queryLocalFonts === 'function' }, layoutType: 'normal', globalNotices: [], @@ -35,6 +45,20 @@ const interfaceMod = { state.settings.currentSaveStateNotice = { error: true, errorData: error } } }, + setTemporaryChanges (state, { timeoutId, confirm, revert }) { + state.temporaryChangesTimeoutId = timeoutId + state.temporaryChangesConfirm = confirm + state.temporaryChangesRevert = revert + }, + clearTemporaryChanges (state) { + clearTimeout(state.temporaryChangesTimeoutId) + state.temporaryChangesTimeoutId = null + state.temporaryChangesConfirm = () => {} + state.temporaryChangesRevert = () => {} + }, + setThemeApplied (state) { + state.themeApplied = true + }, setNotificationPermission (state, permission) { state.notificationPermission = permission }, @@ -86,6 +110,10 @@ const interfaceMod = { }, setLastTimeline (state, value) { state.lastTimeline = value + }, + setFontsList (state, value) { + // Set is used here so that we filter out duplicate fonts (possibly same font but with different weight) + state.localFonts = [...(new Set(value.map(font => font.family))).values()] } }, actions: { @@ -160,10 +188,215 @@ const interfaceMod = { commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile) } }, + queryLocalFonts ({ commit, dispatch, state }) { + if (state.localFonts !== null) return + commit('setFontsList', []) + if (!state.browserSupport.localFonts) { + return + } + window + .queryLocalFonts() + .then((fonts) => { + commit('setFontsList', fonts) + }) + .catch((e) => { + dispatch('pushGlobalNotice', { + messageKey: 'settings.style.themes3.font.font_list_unavailable', + messageArgs: { + error: e + }, + level: 'error' + }) + }) + }, setLastTimeline ({ commit }, value) { commit('setLastTimeline', value) + }, + setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) { + const { + theme: instanceThemeName + } = rootState.instance + + const { + theme: userThemeName, + customTheme: userThemeSnapshot, + customThemeSource: userThemeSource, + forceThemeRecompilation, + themeDebug, + theme3hacks + } = rootState.config + + const actualThemeName = userThemeName || instanceThemeName + + const forceRecompile = forceThemeRecompilation || recompile + + let promise = null + + if (themeData) { + promise = Promise.resolve(normalizeThemeData(themeData)) + } else if (themeName) { + promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData)) + } else if (userThemeSource || userThemeSnapshot) { + promise = Promise.resolve(normalizeThemeData({ + _pleroma_theme_version: 2, + theme: userThemeSnapshot, + source: userThemeSource + })) + } else if (actualThemeName && actualThemeName !== 'custom') { + promise = getPreset(actualThemeName).then(themeData => { + const realThemeData = normalizeThemeData(themeData) + if (actualThemeName === instanceThemeName) { + // This sole line is the reason why this whole block is above the recompilation check + commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } }) + } + return realThemeData + }) + } else { + throw new Error('Cannot load any theme!') + } + + // If we're not not forced to recompile try using + // cache (tryLoadCache return true if load successful) + if (!forceRecompile && !themeDebug && tryLoadCache()) { + commit('setThemeApplied') + return + } + + promise + .then(realThemeData => { + const theme2ruleset = convertTheme2To3(realThemeData) + + if (saveData) { + commit('setOption', { name: 'theme', value: themeName || actualThemeName }) + commit('setOption', { name: 'customTheme', value: realThemeData }) + commit('setOption', { name: 'customThemeSource', value: realThemeData }) + } + const hacks = [] + + Object.entries(theme3hacks).forEach(([key, value]) => { + switch (key) { + case 'fonts': { + Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => { + if (!font?.family) return + switch (fontKey) { + case 'interface': + hacks.push({ + component: 'Root', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'input': + hacks.push({ + component: 'Input', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'post': + hacks.push({ + component: 'RichContent', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'monospace': + hacks.push({ + component: 'Root', + directives: { + '--monoFont': 'generic | ' + font.family + } + }) + break + } + }) + break + } + case 'underlay': { + if (value !== 'none') { + const newRule = { + component: 'Underlay', + directives: {} + } + if (value === 'opaque') { + newRule.directives.opacity = 1 + newRule.directives.background = '--wallpaper' + } + if (value === 'transparent') { + newRule.directives.opacity = 0 + } + hacks.push(newRule) + } + break + } + } + }) + + const ruleset = [ + ...theme2ruleset, + ...hacks + ] + + applyTheme( + ruleset, + () => commit('setThemeApplied'), + themeDebug + ) + }) + + return promise } } } export default interfaceMod + +export const normalizeThemeData = (input) => { + if (Array.isArray(input)) { + const themeData = { colors: {} } + themeData.colors.bg = input[1] + themeData.colors.fg = input[2] + themeData.colors.text = input[3] + themeData.colors.link = input[4] + themeData.colors.cRed = input[5] + themeData.colors.cGreen = input[6] + themeData.colors.cBlue = input[7] + themeData.colors.cOrange = input[8] + return generatePreset(themeData).theme + } + + let themeData, themeSource + + if (input.themeFileVerison === 1) { + // this might not be even used at all, some leftover of unimplemented code in V2 editor + return generatePreset(input).theme + } else if ( + Object.prototype.hasOwnProperty.call(input, '_pleroma_theme_version') || + Object.prototype.hasOwnProperty.call(input, 'source') || + Object.prototype.hasOwnProperty.call(input, 'theme') + ) { + // We got passed a full theme file + themeData = input.theme + themeSource = input.source + } else if (Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion')) { + // We got passed a source/snapshot + themeData = input + themeSource = input + } + // New theme presets don't have 'theme' property, they use 'source' + + let out // shout, shout let it all out + if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) { + // There are some themes in wild that have completely broken source + out = { ...(themeData || {}), ...themeSource } + } else { + out = themeData + } + + // generatePreset here basically creates/updates "snapshot", + // while also fixing the 2.2 -> 2.3 colors/shadows/etc + return generatePreset(out).theme +} diff --git a/src/modules/notifications.js b/src/modules/notifications.js @@ -0,0 +1,169 @@ +import apiService from '../services/api/api.service.js' + +import { + isStatusNotification, + isValidNotification, + maybeShowNotification +} from '../services/notification_utils/notification_utils.js' + +import { + closeDesktopNotification, + closeAllDesktopNotifications +} from '../services/desktop_notification_utils/desktop_notification_utils.js' + +const emptyNotifications = () => ({ + desktopNotificationSilence: true, + maxId: 0, + minId: Number.POSITIVE_INFINITY, + data: [], + idStore: {}, + loading: false +}) + +export const defaultState = () => ({ + ...emptyNotifications() +}) + +export const notifications = { + state: defaultState(), + mutations: { + addNewNotifications (state, { notifications }) { + notifications.forEach(notification => { + state.data.push(notification) + state.idStore[notification.id] = notification + }) + }, + clearNotifications (state) { + state = emptyNotifications() + }, + updateNotificationsMinMaxId (state, id) { + state.maxId = id > state.maxId ? id : state.maxId + state.minId = id < state.minId ? id : state.minId + }, + setNotificationsLoading (state, { value }) { + state.loading = value + }, + setNotificationsSilence (state, { value }) { + state.desktopNotificationSilence = value + }, + markNotificationsAsSeen (state) { + state.data.forEach((notification) => { + notification.seen = true + }) + }, + markSingleNotificationAsSeen (state, { id }) { + const notification = state.idStore[id] + if (notification) notification.seen = true + }, + dismissNotification (state, { id }) { + state.data = state.data.filter(n => n.id !== id) + delete state.idStore[id] + }, + updateNotification (state, { id, updater }) { + const notification = state.idStore[id] + notification && updater(notification) + } + }, + actions: { + addNewNotifications (store, { notifications, older }) { + const { commit, dispatch, state, rootState } = store + const validNotifications = notifications.filter((notification) => { + // If invalid notification, update ids but don't add it to store + if (!isValidNotification(notification)) { + console.error('Invalid notification:', notification) + commit('updateNotificationsMinMaxId', notification.id) + return false + } + return true + }) + + const statusNotifications = validNotifications.filter(notification => isStatusNotification(notification.type) && notification.status) + + // Synchronous commit to add all the statuses + commit('addNewStatuses', { statuses: statusNotifications.map(notification => notification.status) }) + + // Update references to statuses in notifications to ones in the store + statusNotifications.forEach(notification => { + const id = notification.status.id + const referenceStatus = rootState.statuses.allStatusesObject[id] + + if (referenceStatus) { + notification.status = referenceStatus + } + }) + + validNotifications.forEach(notification => { + if (notification.type === 'pleroma:report') { + dispatch('addReport', notification.report) + } + + if (notification.type === 'pleroma:emoji_reaction') { + dispatch('fetchEmojiReactionsBy', notification.status.id) + } + + // Only add a new notification if we don't have one for the same action + // eslint-disable-next-line no-prototype-builtins + if (!state.idStore.hasOwnProperty(notification.id)) { + commit('updateNotificationsMinMaxId', notification.id) + commit('addNewNotifications', { notifications: [notification] }) + + maybeShowNotification(store, notification) + } else if (notification.seen) { + state.idStore[notification.id].seen = true + } + }) + }, + notificationClicked ({ state, dispatch }, { id }) { + const notification = state.idStore[id] + const { type, seen } = notification + + if (!seen) { + switch (type) { + case 'mention': + case 'pleroma:report': + case 'follow_request': + break + default: + dispatch('markSingleNotificationAsSeen', { id }) + } + } + }, + setNotificationsLoading ({ rootState, commit }, { value }) { + commit('setNotificationsLoading', { value }) + }, + setNotificationsSilence ({ rootState, commit }, { value }) { + commit('setNotificationsSilence', { value }) + }, + markNotificationsAsSeen ({ rootState, state, commit }) { + commit('markNotificationsAsSeen') + apiService.markNotificationsAsSeen({ + id: state.maxId, + credentials: rootState.users.currentUser.credentials + }).then(() => { + closeAllDesktopNotifications(rootState) + }) + }, + markSingleNotificationAsSeen ({ rootState, commit }, { id }) { + commit('markSingleNotificationAsSeen', { id }) + apiService.markNotificationsAsSeen({ + single: true, + id, + credentials: rootState.users.currentUser.credentials + }).then(() => { + closeDesktopNotification(rootState, { id }) + }) + }, + dismissNotificationLocal ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + }, + dismissNotification ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + rootState.api.backendInteractor.dismissNotification({ id }) + }, + updateNotification ({ rootState, commit }, { id, updater }) { + commit('updateNotification', { id, updater }) + } + } +} + +export default notifications diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js @@ -419,7 +419,6 @@ const serverSideStorage = { actions: { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { const needPush = state.dirty || force - console.log(needPush) if (!needPush) return commit('updateCache', { username: rootState.users.currentUser.fqn }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } diff --git a/src/modules/statuses.js b/src/modules/statuses.js @@ -12,11 +12,6 @@ import { isArray, omitBy } from 'lodash' -import { - isStatusNotification, - isValidNotification, - maybeShowNotification -} from '../services/notification_utils/notification_utils.js' import apiService from '../services/api/api.service.js' const emptyTl = (userId = 0) => ({ @@ -36,21 +31,12 @@ const emptyTl = (userId = 0) => ({ flushMarker: 0 }) -const emptyNotifications = () => ({ - desktopNotificationSilence: true, - maxId: 0, - minId: Number.POSITIVE_INFINITY, - data: [], - idStore: {}, - loading: false -}) - export const defaultState = () => ({ allStatuses: [], + scrobblesNextFetch: {}, allStatusesObject: {}, conversationsObject: {}, maxId: 0, - notifications: emptyNotifications(), favorites: new Set(), timelines: { mentions: emptyTl(), @@ -120,8 +106,24 @@ const sortTimeline = (timeline) => { return timeline } +const getLatestScrobble = (state, user) => { + if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) { + return + } + + state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000 + apiService.fetchScrobbles({ accountId: user.id }).then((scrobbles) => { + if (scrobbles.length > 0) { + user.latestScrobble = scrobbles[0] + + state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000 + } + }) +} + // Add status to the global storages (arrays and objects maintaining statuses) except timelines const addStatusToGlobalStorage = (state, data) => { + getLatestScrobble(state, data.user) const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data) if (result.new) { // Add to conversation @@ -137,22 +139,6 @@ const addStatusToGlobalStorage = (state, data) => { return result } -// Remove status from the global storages (arrays and objects maintaining statuses) except timelines -const removeStatusFromGlobalStorage = (state, status) => { - remove(state.allStatuses, { id: status.id }) - - // TODO: Need to remove from allStatusesObject? - - // Remove possible notification - remove(state.notifications.data, ({ action: { id } }) => id === status.id) - - // Remove from conversation - const conversationId = status.statusnet_conversation_id - if (state.conversationsObject[conversationId]) { - remove(state.conversationsObject[conversationId], { id: status.id }) - } -} - const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => { // Sanity check if (!isArray(statuses)) { @@ -286,20 +272,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us favoriteStatus(favorite) } }, - deletion: (deletion) => { - const uri = deletion.uri - const status = find(allStatuses, { uri }) - if (!status) { - return - } - - removeStatusFromGlobalStorage(state, status) - - if (timeline) { - remove(timelineObject.statuses, { uri }) - remove(timelineObject.visibleStatuses, { uri }) - } - }, follow: (follow) => { // NOOP, it is known status but we don't do anything about it for now }, @@ -321,52 +293,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } -const updateNotificationsMinMaxId = (state, notification) => { - state.notifications.maxId = notification.id > state.notifications.maxId - ? notification.id - : state.notifications.maxId - state.notifications.minId = notification.id < state.notifications.minId - ? notification.id - : state.notifications.minId -} - -const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => { - each(notifications, (notification) => { - // If invalid notification, update ids but don't add it to store - if (!isValidNotification(notification)) { - console.error('Invalid notification:', notification) - updateNotificationsMinMaxId(state, notification) - return - } - - if (isStatusNotification(notification.type)) { - notification.action = addStatusToGlobalStorage(state, notification.action).item - notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item - } - - if (notification.type === 'pleroma:report') { - dispatch('addReport', notification.report) - } - - if (notification.type === 'pleroma:emoji_reaction') { - dispatch('fetchEmojiReactionsBy', notification.status.id) - } - - // Only add a new notification if we don't have one for the same action - // eslint-disable-next-line no-prototype-builtins - if (!state.notifications.idStore.hasOwnProperty(notification.id)) { - updateNotificationsMinMaxId(state, notification) - - state.notifications.data.push(notification) - state.notifications.idStore[notification.id] = notification - - newNotificationSideEffects(notification) - } else if (notification.seen) { - state.notifications.idStore[notification.id].seen = true - } - }) -} - const removeStatus = (state, { timeline, userId }) => { const timelineObject = state.timelines[timeline] if (userId) { @@ -379,7 +305,6 @@ const removeStatus = (state, { timeline, userId }) => { export const mutations = { addNewStatuses, - addNewNotifications, removeStatus, showNewStatuses (state, { timeline }) { const oldTimeline = (state.timelines[timeline]) @@ -401,9 +326,6 @@ export const mutations = { const userId = excludeUserId ? state.timelines[timeline].userId : undefined state.timelines[timeline] = emptyTl(userId) }, - clearNotifications (state) { - state.notifications = emptyNotifications() - }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -486,31 +408,6 @@ export const mutations = { const newStatus = state.allStatusesObject[id] newStatus.nsfw = nsfw }, - setNotificationsLoading (state, { value }) { - state.notifications.loading = value - }, - setNotificationsSilence (state, { value }) { - state.notifications.desktopNotificationSilence = value - }, - markNotificationsAsSeen (state) { - each(state.notifications.data, (notification) => { - notification.seen = true - }) - }, - markSingleNotificationAsSeen (state, { id }) { - const notification = find(state.notifications.data, n => n.id === id) - if (notification) notification.seen = true - }, - dismissNotification (state, { id }) { - state.notifications.data = state.notifications.data.filter(n => n.id !== id) - }, - dismissNotifications (state, { finder }) { - state.notifications.data = state.notifications.data.filter(n => finder) - }, - updateNotification (state, { id, updater }) { - const notification = find(state.notifications.data, n => n.id === id) - notification && updater(notification) - }, queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id }, @@ -592,23 +489,9 @@ export const mutations = { const statuses = { state: defaultState(), actions: { - addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { + addNewStatuses ({ rootState, commit, dispatch, state }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination }) }, - addNewNotifications (store, { notifications, older }) { - const { commit, dispatch, rootGetters } = store - - const newNotificationSideEffects = (notification) => { - maybeShowNotification(store, notification) - } - commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects }) - }, - setNotificationsLoading ({ rootState, commit }, { value }) { - commit('setNotificationsLoading', { value }) - }, - setNotificationsSilence ({ rootState, commit }, { value }) { - commit('setNotificationsSilence', { value }) - }, fetchStatus ({ rootState, dispatch }, id) { return rootState.api.backendInteractor.fetchStatus({ id }) .then((status) => dispatch('addNewStatuses', { statuses: [status] })) @@ -704,31 +587,6 @@ const statuses = { queueFlushAll ({ rootState, commit }) { commit('queueFlushAll') }, - markNotificationsAsSeen ({ rootState, commit }) { - commit('markNotificationsAsSeen') - apiService.markNotificationsAsSeen({ - id: rootState.statuses.notifications.maxId, - credentials: rootState.users.currentUser.credentials - }) - }, - markSingleNotificationAsSeen ({ rootState, commit }, { id }) { - commit('markSingleNotificationAsSeen', { id }) - apiService.markNotificationsAsSeen({ - single: true, - id, - credentials: rootState.users.currentUser.credentials - }) - }, - dismissNotificationLocal ({ rootState, commit }, { id }) { - commit('dismissNotification', { id }) - }, - dismissNotification ({ rootState, commit }, { id }) { - commit('dismissNotification', { id }) - rootState.api.backendInteractor.dismissNotification({ id }) - }, - updateNotification ({ rootState, commit }, { id, updater }) { - commit('updateNotification', { id, updater }) - }, fetchFavsAndRepeats ({ rootState, commit }, id) { Promise.all([ rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), diff --git a/src/modules/users.js b/src/modules/users.js @@ -2,7 +2,7 @@ import backendInteractorService from '../services/backend_interactor_service/bac import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import oauthApi from '../services/new_api/oauth.js' import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' -import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' +import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -250,6 +250,7 @@ export const mutations = { signUpPending (state) { state.signUpPending = true state.signUpErrors = [] + state.signUpNotice = {} }, signUpSuccess (state) { state.signUpPending = false @@ -257,6 +258,12 @@ export const mutations = { signUpFailure (state, errors) { state.signUpPending = false state.signUpErrors = errors + state.signUpNotice = {} + }, + signUpNotice (state, notice) { + state.signUpPending = false + state.signUpErrors = [] + state.signUpNotice = notice } } @@ -287,6 +294,7 @@ export const defaultState = { usersByNameObject: {}, signUpPending: false, signUpErrors: [], + signUpNotice: {}, relationships: {} } @@ -498,7 +506,7 @@ const users = { store.commit('addNewUsers', users) store.commit('addNewUsers', targetUsers) - const notificationsObject = store.rootState.statuses.notifications.idStore + const notificationsObject = store.rootState.notifications.idStore const relevantNotifications = Object.entries(notificationsObject) .filter(([k, val]) => notificationIds.includes(k)) .map(([k, val]) => val) @@ -524,9 +532,16 @@ const users = { const data = await rootState.api.backendInteractor.register( { params: { ...userInfo } } ) - store.commit('signUpSuccess') - store.commit('setToken', data.access_token) - store.dispatch('loginUser', data.access_token) + + if (data.access_token) { + store.commit('signUpSuccess') + store.commit('setToken', data.access_token) + store.dispatch('loginUser', data.access_token) + return 'ok' + } else { // Request succeeded, but user cannot login yet. + store.commit('signUpNotice', data) + return 'request_sent' + } } catch (e) { const errors = e.message store.commit('signUpFailure', errors) @@ -667,7 +682,7 @@ const users = { resolve() }) .catch((error) => { - console.log(error) + console.error(error) commit('endLogin') reject(new Error('Failed to connect to server, try again')) }) diff --git a/src/panel.scss b/src/panel.scss @@ -1,15 +1,24 @@ /* stylelint-disable no-descending-specificity */ .panel { + --__panel-background: var(--background); + --__panel-backdrop-filter: var(--backdrop-filter); + + .tab-switcher .tabs { + background: var(--__panel-background); + backdrop-filter: var(--__panel-backdrop-filter); + } + position: relative; display: flex; flex-direction: column; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + + .panel-heading { + background-color: inherit; + } &::after, & { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); } &::after { @@ -20,19 +29,25 @@ left: 0; right: 0; z-index: 5; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); pointer-events: none; } } .panel-body { padding: var(--panel-body-padding, 0); + background: var(--background); + backdrop-filter: var(--__panel-backdrop-filter); + + .tab-switcher .tabs { + background: none; + backdrop-filter: none; + } &:empty::before { content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations display: block; - margin: 1em; + padding: 1em; text-align: center; } @@ -45,11 +60,13 @@ .panel-heading, .panel-footer { - --panel-heading-height-padding: 0.6em; - --__panel-heading-gap: 0.5em; - --__panel-heading-height: 3.2em; + --panel-heading-height-padding: calc(var(--panel-header-height) * 0.2); + --__panel-heading-gap: calc(var(--panel-header-height) * 0.1565); + --__panel-heading-height: var(--panel-header-height); --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0)); + font-size: calc(var(--panelHeaderSize) / 3.2); + backdrop-filter: var(--__panel-backdrop-filter); position: relative; box-sizing: border-box; display: grid; @@ -76,8 +93,7 @@ &.-stub { &, &::after { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); } } @@ -119,82 +135,33 @@ padding-bottom: 0; align-self: stretch; } + + > .alert { + line-height: calc(var(--__panel-heading-height-inner) - 2px); + } } } // TODO Should refactor panels into separate component and utilize slots .panel-heading { - border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; - border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; + border-radius: var(--roundness) var(--roundness) 0 0; border-width: 0 0 1px; align-items: start; - // panel theme - color: var(--panelText); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + background-image: + linear-gradient(to bottom, var(--background), var(--background)), + linear-gradient(to bottom, var(--__panel-background), var(--__panel-background)); &::after { - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + background-color: var(--background); z-index: -2; - border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; - border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; - box-shadow: var(--panelHeaderShadow); - } - - a, - .-link { - color: $fallback--link; - color: var(--panelLink, $fallback--link); - } - - .button-unstyled:hover, - a:hover { - i[class*="icon-"], - .svg-inline--fa, - .iconLetter { - color: var(--panelText); - } - } - - .faint { - background-color: transparent; - color: $fallback--faint; - color: var(--panelFaint, $fallback--faint); - } - - .faint-link { - color: $fallback--faint; - color: var(--faintLink, $fallback--faint); + border-radius: var(--roundness) var(--roundness) 0 0; + box-shadow: var(--shadow); } &:not(.-flexible-height) { > .button-default { flex-shrink: 0; - - &, - i[class*="icon-"] { - color: $fallback--text; - color: var(--btnPanelText, $fallback--text); - } - - &:active { - background-color: $fallback--fg; - background-color: var(--btnPressedPanel, $fallback--fg); - color: $fallback--text; - color: var(--btnPressedPanelText, $fallback--text); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledPanelText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledPanelText, $fallback--text); - } } } @@ -232,11 +199,12 @@ } .panel-footer { - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: 0; + border-top-right-radius: 0; align-items: center; border-width: 1px 0 0; border-style: solid; - border-color: var(--border, $fallback--border); + border-color: var(--border); + background-color: var(--__panel-background); } /* stylelint-enable no-descending-specificity */ diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -107,12 +107,24 @@ const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` +const PLEROMA_SCROBBLES_URL = id => `/api/v1/pleroma/accounts/${id}/scrobbles` +const PLEROMA_STATUS_QUOTES_URL = id => `/api/v1/pleroma/statuses/${id}/quotes` +const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id}/favourites` const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config' const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions' const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends' const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install' +const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' +const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' +const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}` +const PLEROMA_EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}` +const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' +const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL = + (url, page, pageSize) => `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` +const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => `/api/v1/pleroma/emoji/packs/files?name=${name}` + const oldfetch = window.fetch const fetch = (url, options) => { @@ -670,9 +682,11 @@ const fetchTimeline = ({ timeline, credentials, since = false, + minId = false, until = false, userId = false, listId = false, + statusId = false, tag = false, withMuted = false, replyVisibility = 'all', @@ -688,14 +702,20 @@ const fetchTimeline = ({ media: MASTODON_USER_TIMELINE_URL, list: MASTODON_LIST_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, + publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, tag: MASTODON_TAG_TIMELINE_URL, - bookmarks: MASTODON_BOOKMARK_TIMELINE_URL + bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, + quotes: PLEROMA_STATUS_QUOTES_URL } const isNotifications = timeline === 'notifications' const params = [] let url = timelineUrls[timeline] + if (timeline === 'favorites' && userId) { + url = timelineUrls.publicFavorites(userId) + } + if (timeline === 'user' || timeline === 'media') { url = url(userId) } @@ -704,6 +724,13 @@ const fetchTimeline = ({ url = url(listId) } + if (timeline === 'quotes') { + url = url(statusId) + } + + if (minId) { + params.push(['min_id', minId]) + } if (since) { params.push(['since_id', since]) } @@ -1765,6 +1792,107 @@ const installFrontend = ({ credentials, payload }) => { }) } +const fetchScrobbles = ({ accountId, limit = 1 }) => { + let url = PLEROMA_SCROBBLES_URL(accountId) + const params = [['limit', limit]] + const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') + url += `?${queryString}` + return fetch(url, {}) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const deleteEmojiPack = ({ name }) => { + return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' }) +} + +const reloadEmoji = () => { + return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' }) +} + +const importEmojiFromFS = () => { + return fetch(PLEROMA_EMOJI_IMPORT_FS_URL) +} + +const createEmojiPack = ({ name }) => { + return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' }) +} + +const listEmojiPacks = ({ page, pageSize }) => { + return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize)) +} + +const listRemoteEmojiPacks = ({ instance, page, pageSize }) => { + if (!instance.startsWith('http')) { + instance = 'https://' + instance + } + + return fetch( + PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), + { + headers: { 'Content-Type': 'application/json' } + } + ) +} + +const downloadRemoteEmojiPack = ({ instance, packName, as }) => { + return fetch( + PLEROMA_EMOJI_PACKS_DL_REMOTE_URL, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: instance, name: packName, as + }) + } + ) +} + +const saveEmojiPackMetadata = ({ name, newData }) => { + return fetch( + PLEROMA_EMOJI_PACK_URL(name), + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ metadata: newData }) + } + ) +} + +const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { + const data = new FormData() + if (filename.trim() !== '') { data.set('filename', filename) } + if (shortcode.trim() !== '') { data.set('shortcode', shortcode) } + data.set('file', file) + + return fetch( + PLEROMA_EMOJI_UPDATE_FILE_URL(packName), + { method: 'POST', body: data } + ) +} + +const updateEmojiFile = ({ packName, shortcode, newShortcode, newFilename, force }) => { + return fetch( + PLEROMA_EMOJI_UPDATE_FILE_URL(packName), + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ shortcode, new_shortcode: newShortcode, new_filename: newFilename, force }) + } + ) +} + +const deleteEmojiFile = ({ packName, shortcode }) => { + return fetch(`${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, { method: 'DELETE' }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -1878,12 +2006,24 @@ const apiService = { postAnnouncement, editAnnouncement, deleteAnnouncement, + fetchScrobbles, adminFetchAnnouncements, fetchInstanceDBConfig, fetchInstanceConfigDescriptions, fetchAvailableFrontends, pushInstanceDBConfig, - installFrontend + installFrontend, + importEmojiFromFS, + reloadEmoji, + listEmojiPacks, + createEmojiPack, + deleteEmojiPack, + saveEmojiPackMetadata, + addNewEmojiFile, + updateEmojiFile, + deleteEmojiFile, + listRemoteEmojiPacks, + downloadRemoteEmojiPack } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js @@ -5,8 +5,8 @@ import followRequestFetcher from '../../services/follow_request_fetcher/follow_r import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' const backendInteractorService = credentials => ({ - startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) { - return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, tag }) + startFetchingTimeline ({ timeline, store, userId = false, listId = false, statusId = false, tag }) { + return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, statusId, tag }) }, fetchTimeline (args) { diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js @@ -173,7 +173,7 @@ export const mixrgb = (a, b) => { * @returns {String} CSS rgba() color */ export const rgba2css = function (rgba) { - return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})` + return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a ?? 1})` } /** diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js @@ -1,9 +1,38 @@ +import { + showDesktopNotification as swDesktopNotification, + closeDesktopNotification as swCloseDesktopNotification, + 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.statuses.notifications.desktopNotificationSilence) { return } + if (rootState.notifications.desktopNotificationSilence) { return } + + if (isSWSupported()) { + swDesktopNotification(desktopNotificationOpts) + } else if (!state.failCreateNotif) { + try { + const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts) + setTimeout(desktopNotification.close.bind(desktopNotification), 5000) + } catch { + state.failCreateNotif = true + } + } +} + +export const closeDesktopNotification = (rootState, { id }) => { + if (!('Notification' in window && window.Notification.permission === 'granted')) return + + if (isSWSupported()) { + swCloseDesktopNotification({ id }) + } +} + +export const closeAllDesktopNotifications = (rootState) => { + if (!('Notification' in window && window.Notification.permission === 'granted')) return - const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts) - // Chrome is known for not closing notifications automatically - // according to MDN, anyway. - setTimeout(desktopNotification.close.bind(desktopNotification), 5000) + if (isSWSupported()) { + swCloseDesktopNotification({}) + } } diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -107,6 +107,7 @@ export const parseUser = (data) => { output.allow_following_move = data.pleroma.allow_following_move + output.hide_favorites = data.pleroma.hide_favorites output.hide_follows = data.pleroma.hide_follows output.hide_followers = data.pleroma.hide_followers output.hide_follows_count = data.pleroma.hide_follows_count @@ -165,6 +166,7 @@ export const parseUser = (data) => { output.show_role = data.source.pleroma.show_role output.discoverable = data.source.pleroma.discoverable output.show_birthday = data.pleroma.show_birthday + output.actor_type = data.source.pleroma.actor_type } } @@ -329,6 +331,7 @@ export const parseStatus = (data) => { output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined) output.quote_url = pleroma.quote_url output.quote_visible = pleroma.quote_visible + output.quotes_count = pleroma.quotes_count } else { output.text = data.content output.summary = data.spoiler_text @@ -439,7 +442,6 @@ export const parseNotification = (data) => { output.type = mastoDict[data.type] || data.type output.seen = data.pleroma.is_seen output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null - output.action = output.status // TODO: Refactor, this is unneeded output.target = output.type !== 'move' ? null : parseUser(data.target) diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js @@ -55,10 +55,13 @@ const createFaviconService = () => { }) } + const getOriginalFavicons = () => [...favicons] + return { initFaviconService, clearFaviconBadge, - drawFaviconBadge + drawFaviconBadge, + getOriginalFavicons } } diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js @@ -26,7 +26,7 @@ export const fileType = mimetype => { } export const fileTypeExt = url => { - if (url.match(/\.(png|jpe?g|gif|webp|avif)$/)) { + if (url.match(/\.(a?png|jpe?g|gif|webp|avif)$/)) { return 'image' } if (url.match(/\.(ogv|mp4|webm|mov)$/)) { diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js @@ -1,28 +1,37 @@ -import { filter, sortBy, includes } from 'lodash' import { muteWordHits } from '../status_parser/status_parser.js' import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' -export const notificationsFromStore = store => store.state.statuses.notifications.data +import FaviconService from 'src/services/favicon_service/favicon_service.js' + +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 visibleTypes = store => { - const rootState = store.rootState || store.state + // 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 ([ - rootState.config.notificationVisibility.likes && 'like', - rootState.config.notificationVisibility.mentions && 'mention', - rootState.config.notificationVisibility.repeats && 'repeat', - rootState.config.notificationVisibility.follows && 'follow', - rootState.config.notificationVisibility.followRequest && 'follow_request', - rootState.config.notificationVisibility.moves && 'move', - rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction', - rootState.config.notificationVisibility.reports && 'pleroma:report', - rootState.config.notificationVisibility.polls && 'poll' + notificationVisibility.likes && 'like', + notificationVisibility.mentions && 'mention', + notificationVisibility.statuses && 'status', + notificationVisibility.repeats && 'repeat', + notificationVisibility.follows && 'follow', + notificationVisibility.followRequest && 'follow_request', + notificationVisibility.moves && 'move', + notificationVisibility.emojiReactions && 'pleroma:emoji_reaction', + notificationVisibility.reports && 'pleroma:report', + notificationVisibility.polls && 'poll' ].filter(_ => _)) } -const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll'] +const statusNotifications = new Set(['like', 'mention', 'status', 'repeat', 'pleroma:emoji_reaction', 'poll']) -export const isStatusNotification = (type) => includes(statusNotifications, type) +export const isStatusNotification = (type) => statusNotifications.has(type) export const isValidNotification = (notification) => { if (isStatusNotification(notification.type) && !notification.status) { @@ -49,35 +58,57 @@ const sortById = (a, b) => { const isMutedNotification = (store, notification) => { if (!notification.status) return - return notification.status.muted || muteWordHits(notification.status, store.rootGetters.mergedConfig.muteWords).length > 0 + const rootGetters = store.rootGetters || store.getters + return notification.status.muted || muteWordHits(notification.status, rootGetters.mergedConfig.muteWords).length > 0 } export const maybeShowNotification = (store, notification) => { const rootState = store.rootState || store.state + const rootGetters = store.rootGetters || store.getters if (notification.seen) return if (!visibleTypes(store).includes(notification.type)) return if (notification.type === 'mention' && isMutedNotification(store, notification)) return - const notificationObject = prepareNotificationObject(notification, store.rootGetters.i18n) + const notificationObject = prepareNotificationObject(notification, rootGetters.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 - let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById) - sortedNotifications = sortBy(sortedNotifications, 'seen') + 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) ) } -export const unseenNotificationsFromStore = store => - filter(filteredNotificationsFromStore(store), ({ seen }) => !seen) +export const unseenNotificationsFromStore = store => { + const rootGetters = store.rootGetters || store.getters + const ignoreInactionableSeen = rootGetters.mergedConfig.ignoreInactionableSeen + + return filteredNotificationsFromStore(store).filter(({ seen, type }) => { + if (!ignoreInactionableSeen) return !seen + if (seen) return false + return ACTIONABLE_NOTIFICATION_TYPES.has(type) + }) +} export const prepareNotificationObject = (notification, i18n) => { + if (cachedBadgeUrl === null) { + const favicons = FaviconService.getOriginalFavicons() + const favicon = favicons[favicons.length - 1] + if (!favicon) { + cachedBadgeUrl = 'about:blank' + } else { + cachedBadgeUrl = favicon.favimg.src + } + } + const notifObj = { - tag: notification.id + tag: notification.id, + type: notification.type, + badge: cachedBadgeUrl } const status = notification.status const title = notification.from_profile.name @@ -88,6 +119,9 @@ export const prepareNotificationObject = (notification, i18n) => { case 'like': i18nString = 'favorited_you' break + case 'status': + i18nString = 'subscribed_status' + break case 'repeat': i18nString = 'repeated_you' break @@ -124,3 +158,18 @@ export const prepareNotificationObject = (notification, i18n) => { return notifObj } + +export const countExtraNotifications = (store) => { + const rootGetters = store.rootGetters || store.getters + const mergedConfig = rootGetters.mergedConfig + + if (!mergedConfig.showExtraNotifications) { + return 0 + } + + return [ + mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0, + mergedConfig.showAnnouncementsInExtraNotifications ? rootGetters.unreadAnnouncementCount : 0, + mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0 + ].reduce((a, c) => a + c, 0) +} diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -5,11 +5,15 @@ import { promiseInterval } from '../promise_interval/promise_interval.js' // Note: chat_mention excluded as pleroma-fe polls them separately const mastoApiNotificationTypes = [ 'mention', + 'status', 'favourite', 'reblog', 'follow', + 'follow_request', 'move', + 'poll', 'pleroma:emoji_reaction', + 'pleroma:chat_mention', 'pleroma:report' ] @@ -21,7 +25,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const args = { credentials } const { getters } = store const rootState = store.rootState || store.state - const timelineData = rootState.statuses.notifications + const timelineData = rootState.notifications const hideMutedPosts = getters.mergedConfig.hideMutedPosts args.includeTypes = mastoApiNotificationTypes @@ -49,10 +53,14 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { // 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 numUnseenNotifs = notifications.length - readNotifsIds.length - if (numUnseenNotifs > 0 && readNotifsIds.length > 0) { - args.since = Math.max(...readNotifsIds) - fetchNotifications({ store, args, older }) + 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) { + args.since = false // Don't use since_id since it sorta conflicts with min_id + args.minId = minId - 1 // go beyond + fetchNotifications({ store, args, older }) + } } return result diff --git a/src/services/push/push.js b/src/services/push/push.js @@ -1,111 +0,0 @@ -import runtime from 'serviceworker-webpack5-plugin/lib/runtime' - -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))) -} - -function isPushSupported () { - return 'serviceWorker' in navigator && 'PushManager' in window -} - -function getOrCreateServiceWorker () { - return runtime.register() - .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')) - - const subscribeOptions = { - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) - } - return registration.pushManager.subscribe(subscribeOptions) -} - -function unsubscribePush (registration) { - return registration.pushManager.getSubscription() - .then((subscribtion) => { - if (subscribtion === null) { return } - return subscribtion.unsubscribe() - }) -} - -function deleteSubscriptionFromBackEnd (token) { - return window.fetch('/api/v1/push/subscription/', { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 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 - } - } - }) - }).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 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}`)) - } -} - -export function unregisterPushNotifications (token) { - if (isPushSupported()) { - Promise.all([ - deleteSubscriptionFromBackEnd(token), - getOrCreateServiceWorker() - .then((registration) => { - return unsubscribePush(registration).then((result) => [registration, result]) - }) - .then(([registration, unsubResult]) => { - if (!unsubResult) { - console.warn('Push subscription cancellation wasn\'t successful, killing SW anyway...') - } - return registration.unregister().then((result) => { - if (!result) { - console.warn('Failed to kill SW') - } - }) - }) - ]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`)) - } -} diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -1,356 +1,259 @@ -import { convert } from 'chromatism' -import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js' -import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js' +import { hex2rgb } from '../color_convert/color_convert.js' +import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js' +import { getCssRules } from '../theme_data/css_utils.js' import { defaultState } from '../../modules/config.js' +import { chunk } from 'lodash' + +// On platforms where this is not supported, it will return undefined +// Otherwise it will return an array +const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets + +const createStyleSheet = (id) => { + if (supportsAdoptedStyleSheets) { + return { + el: null, + sheet: new CSSStyleSheet(), + rules: [] + } + } -export const applyTheme = (input) => { - const { rules } = generatePreset(input) - const head = document.head - const body = document.body - body.classList.add('hidden') - - const styleEl = document.createElement('style') - head.appendChild(styleEl) - const styleSheet = styleEl.sheet + const el = document.getElementById(id) + // Clear all rules in it + for (let i = el.sheet.cssRules.length - 1; i >= 0; --i) { + el.sheet.deleteRule(i) + } - styleSheet.toString() - styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') - body.classList.remove('hidden') + return { + el, + sheet: el.sheet, + rules: [] + } } -const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) => - ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) - -const defaultConfigColumns = configColumns(defaultState) - -export const applyConfig = (config) => { - const columns = configColumns(config) +const EAGER_STYLE_ID = 'pleroma-eager-styles' +const LAZY_STYLE_ID = 'pleroma-lazy-styles' - if (columns === defaultConfigColumns) { - return +const adoptStyleSheets = (styles) => { + if (supportsAdoptedStyleSheets) { + document.adoptedStyleSheets = styles.map(s => s.sheet) } + // Some older browsers do not support document.adoptedStyleSheets. + // In this case, we use the <style> elements. + // Since the <style> elements we need are already in the DOM, there + // is nothing to do here. +} - const head = document.head - const body = document.body - body.classList.add('hidden') - - const rules = Object - .entries(columns) - .filter(([k, v]) => v) - .map(([k, v]) => `--${k}: ${v}`).join(';') +export const generateTheme = async (inputRuleset, callbacks, debug) => { + const { + onNewRule = (rule, isLazy) => {}, + onLazyFinished = () => {}, + onEagerFinished = () => {} + } = callbacks + + // Assuming that "worst case scenario background" is panel background since it's the most likely one + const themes3 = init({ + inputRuleset, + ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(), + debug + }) - const styleEl = document.createElement('style') - head.appendChild(styleEl) - const styleSheet = styleEl.sheet + getCssRules(themes3.eager, debug).forEach(rule => { + // Hacks to support multiple selectors on same component + if (rule.match(/::-webkit-scrollbar-button/)) { + const parts = rule.split(/[{}]/g) + const newRule = [ + parts[0], + ', ', + parts[0].replace(/button/, 'thumb'), + ', ', + parts[0].replace(/scrollbar-button/, 'resizer'), + ' {', + parts[1], + '}' + ].join('') + onNewRule(newRule, false) + } else { + onNewRule(rule, false) + } + }) + onEagerFinished() + + // Optimization - instead of processing all lazy rules in one go, process them in small chunks + // so that UI can do other things and be somewhat responsive while less important rules are being + // processed + let counter = 0 + const chunks = chunk(themes3.lazy, 200) + // 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 => { + if (rule.match(/\.modal-view/)) { + const parts = rule.split(/[{}]/g) + const newRule = [ + parts[0], + ', ', + parts[0].replace(/\.modal-view/, '#modal'), + ', ', + parts[0].replace(/\.modal-view/, '.shout-panel'), + ' {', + parts[1], + '}' + ].join('') + onNewRule(newRule, true) + } else { + onNewRule(rule, true) + } + }) + // const t1 = performance.now() + // console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms') + // t0 = t1 + counter += 1 + if (counter < chunks.length) { + setTimeout(processChunk, 0) + } else { + onLazyFinished() + } + }) + } - styleSheet.toString() - styleSheet.insertRule(`:root { ${rules} }`, 'index-max') - body.classList.remove('hidden') + return { lazyProcessFunc: processChunk } } -export const getCssShadow = (input, usesDropShadow) => { - if (input.length === 0) { - return 'none' +export const tryLoadCache = () => { + const json = localStorage.getItem('pleroma-fe-theme-cache') + if (!json) return null + let cache + try { + cache = JSON.parse(json) + } catch (e) { + console.error('Failed to decode theme cache:', e) + return false } + if (cache.engineChecksum === getEngineChecksum()) { + const eagerStyles = createStyleSheet(EAGER_STYLE_ID) + const lazyStyles = createStyleSheet(LAZY_STYLE_ID) - 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(', ') -} + cache.data[0].forEach(rule => eagerStyles.sheet.insertRule(rule, 'index-max')) + cache.data[1].forEach(rule => lazyStyles.sheet.insertRule(rule, 'index-max')) -const getCssShadowFilter = (input) => { - if (input.length === 0) { - return 'none' - } + adoptStyleSheets([eagerStyles, lazyStyles]) - 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 true + } else { + console.warn('Engine checksum doesn\'t match, cache not usable, clearing') + localStorage.removeItem('pleroma-fe-theme-cache') + } } -export const generateColors = (themeData) => { - const sourceColors = !themeData.themeEngineVersion - ? colors2to3(themeData.colors || themeData) - : themeData.colors || themeData +export const applyTheme = async (input, onFinish = (data) => {}, debug) => { + const eagerStyles = createStyleSheet(EAGER_STYLE_ID) + const lazyStyles = createStyleSheet(LAZY_STYLE_ID) + + const { lazyProcessFunc } = await generateTheme( + input, + { + onNewRule (rule, isLazy) { + if (isLazy) { + lazyStyles.sheet.insertRule(rule, 'index-max') + lazyStyles.rules.push(rule) + } else { + eagerStyles.sheet.insertRule(rule, 'index-max') + eagerStyles.rules.push(rule) + } + }, + onEagerFinished () { + adoptStyleSheets([eagerStyles]) + }, + onLazyFinished () { + adoptStyleSheets([eagerStyles, lazyStyles]) + const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] } + onFinish(cache) + localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) + } + }, + debug + ) - const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}) + setTimeout(lazyProcessFunc, 0) - 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: {} }) - return { - rules: { - colors: Object.entries(htmlColors.complete) - .filter(([k, v]) => v) - .map(([k, v]) => `--${k}: ${v}`) - .join(';') - }, - theme: { - colors: htmlColors.solid, - opacity - } - } + return Promise.resolve() } -export const generateRadii = (input) => { - let inputRadii = input.radii || {} - // v1 -> v2 - if (typeof input.btnRadius !== 'undefined') { - inputRadii = Object - .entries(input) - .filter(([k, v]) => k.endsWith('Radius')) - .reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {}) +const extractStyleConfig = ({ + sidebarColumnWidth, + contentColumnWidth, + notifsColumnWidth, + emojiReactionsScale, + emojiSize, + navbarSize, + panelHeaderSize, + textSize, + forcedRoundness +}) => { + const result = { + sidebarColumnWidth, + contentColumnWidth, + notifsColumnWidth, + emojiReactionsScale, + emojiSize, + navbarSize, + panelHeaderSize, + textSize } - const radii = Object.entries(inputRadii).filter(([k, 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(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';') - }, - theme: { - radii - } + switch (forcedRoundness) { + case 'disable': + break + case '0': + result.forcedRoundness = '0' + break + case '1': + result.forcedRoundness = '1px' + break + case '2': + result.forcedRoundness = '0.4rem' + break + default: } + + return result } -export const generateFonts = (input) => { - const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => { - acc[k] = Object.entries(v).filter(([k, 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 defaultStyleConfig = extractStyleConfig(defaultState) - return { - rules: { - fonts: Object - .entries(fonts) - .filter(([k, v]) => v) - .map(([k, v]) => `--${k}Font: ${v.family}`).join(';') - }, - theme: { - fonts - } +export const applyConfig = (input) => { + const config = extractStyleConfig(input) + + if (config === defaultStyleConfig) { + return } -} -const border = (top, shadow) => ({ - x: 0, - y: top ? 1 : -1, - blur: 0, - spread: 0, - color: shadow ? '#000000' : '#FFFFFF', - alpha: 0.2, - inset: true -}) -const buttonInsetFakeBorders = [border(true, false), border(false, true)] -const inputInsetFakeBorders = [border(true, true), border(false, false)] -const hoverGlow = { - x: 0, - y: 0, - blur: 4, - spread: 0, - color: '--faint', - alpha: 1 -} + const head = document.head + const body = document.body + body.classList.add('hidden') -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 - }], - avatarStatus: [], - panelHeader: [], - 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 - }] -} -export const generateShadows = (input, colors) => { - // TODO this is a small hack for `mod` to work with shadows - // this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element - const hackContextDict = { - button: 'btn', - panel: 'bg', - top: 'topBar', - popup: 'popover', - avatar: 'bg', - panelHeader: 'panel', - input: 'input' - } + const rules = Object + .entries(config) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}: ${v}`).join(';') - 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 })) - ]) - ) - const inputShadows = cleanInputShadows && !input.themeEngineVersion - ? shadows2to3(cleanInputShadows, input.opacity) - : cleanInputShadows || {} - const shadows = Object.entries({ - ...DEFAULT_SHADOWS, - ...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 mod = isLightOnDark ? 1 : -1 - const newShadow = shadowDefs.reduce((shadowAcc, def) => [ - ...shadowAcc, - { - ...def, - color: rgb2hex(computeDynamicColor( - def.color, - (variableSlot) => convert(colors[variableSlot]).rgb, - mod - )) - } - ], []) - return { ...shadowsAcc, [slotName]: newShadow } - }, {}) + document.getElementById('style-config')?.remove() + const styleEl = document.createElement('style') + styleEl.id = 'style-config' + head.appendChild(styleEl) + const styleSheet = styleEl.sheet - 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(';') - }, - theme: { - shadows - } - } -} + styleSheet.toString() + styleSheet.insertRule(`:root { ${rules} }`, 'index-max') -export const composePreset = (colors, radii, shadows, fonts) => { - return { - rules: { - ...shadows.rules, - ...colors.rules, - ...radii.rules, - ...fonts.rules - }, - theme: { - ...shadows.theme, - ...colors.theme, - ...radii.theme, - ...fonts.theme - } + if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) { + styleSheet.insertRule(` * { + --roundness: var(--forcedRoundness) !important; + }`, 'index-max') } -} -export const generatePreset = (input) => { - const colors = generateColors(input) - return composePreset( - colors, - generateRadii(input), - generateShadows(input, colors.theme.colors, colors.mod), - generateFonts(input) - ) + body.classList.remove('hidden') } export const getThemes = () => { @@ -382,47 +285,6 @@ export const getThemes = () => { }, {}) }) } -export const colors2to3 = (colors) => { - return Object.entries(colors).reduce((acc, [slotName, color]) => { - const btnPositions = ['', 'Panel', 'TopBar'] - switch (slotName) { - case 'lightBg': - return { ...acc, highlight: color } - case 'btnText': - return { - ...acc, - ...btnPositions - .reduce( - (statePositionAcc, position) => - ({ ...statePositionAcc, ['btn' + position + 'Text']: color }) - , {} - ) - } - default: - return { ...acc, [slotName]: color } - } - }, {}) -} - -/** - * This handles compatibility issues when importing v2 theme's shadows to current format - * - * 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 } - }, {}) -} export const getPreset = (val) => { return getThemes() @@ -448,5 +310,3 @@ export const getPreset = (val) => { return { theme: data, source: theme.source } }) } - -export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme)) diff --git a/src/services/sw/sw.js b/src/services/sw/sw.js @@ -0,0 +1,148 @@ +import runtime from 'serviceworker-webpack5-plugin/lib/runtime' + +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 () { + return 'serviceWorker' in navigator +} + +function isPushSupported () { + return 'PushManager' in window +} + +function getOrCreateServiceWorker () { + return runtime.register() + .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')) + + const subscribeOptions = { + userVisibleOnly: false, + 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 deleteSubscriptionFromBackEnd (token) { + return fetch('/api/v1/push/subscription/', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 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 + } + } + }) + }).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) { + if (!isSWSupported()) return + await getOrCreateServiceWorker() + navigator.serviceWorker.addEventListener('message', (event) => { + const { dispatch } = store + const { type, ...rest } = event.data + + switch (type) { + case 'notificationClicked': + dispatch('notificationClicked', { id: rest.id }) + } + }) +} + +export async function showDesktopNotification (content) { + if (!isSWSupported) return + 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 }) { + if (!isSWSupported) return + 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 } }) + } else { + sw.postMessage({ type: 'desktopNotificationClose', content: { all: true } }) + } +} + +export async function updateFocus () { + if (!isSWSupported) return + 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) { + 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}`)) + } +} + +export function unregisterPushNotifications (token) { + if (isPushSupported()) { + Promise.all([ + deleteSubscriptionFromBackEnd(token), + getOrCreateServiceWorker() + .then((registration) => { + return unsubscribePush(registration).then((result) => [registration, result]) + }) + .then(([registration, unsubResult]) => { + if (!unsubResult) { + console.warn('Push subscription cancellation wasn\'t successful') + } + }) + ]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`)) + } +} diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js @@ -0,0 +1,173 @@ +import { convert } from 'chromatism' + +import { hex2rgb, rgba2css } from '../color_convert/color_convert.js' + +export const parseCssShadow = (text) => { + const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0] + const inset = /inset/.exec(text)?.[0] + const color = text.replace(dimensions, '').replace(inset, '') + + const [x, y, blur = 0, spread = 0] = dimensions.split(/ /).filter(x => x).map(x => x.trim()) + const isInset = inset?.trim() === 'inset' + const colorString = color.split(/ /).filter(x => x).map(x => x.trim())[0] + + return { + x, + y, + blur, + spread, + inset: isInset, + color: colorString + } +} + +export const getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha }) + +export const getCssShadow = (input, usesDropShadow) => { + if (input.length === 0) { + return 'none' + } + + 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(', ') +} + +export const getCssShadowFilter = (input) => { + if (input.length === 0) { + 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(' ') +} + +// `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 ') + } + case 'shadow': { + 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 [] + 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()) // woah, Extreme! + 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 '' + } + } + return '' + } + }).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 + ].join('\n') +}).filter(x => x) + +export const getScopedVersion = (rules, newScope) => { + return rules.map(x => { + if (x.startsWith('html')) { + return x.replace('html', newScope) + } else if (x.startsWith('#content')) { + return x.replace('#content', newScope) + } else { + return newScope + ' > ' + x + } + }) +} diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js @@ -0,0 +1,168 @@ +import { sortBy } from 'lodash' + +// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }} +// into an array [item2, item3] for iterating +export const unroll = (item) => { + const out = [] + let currentParent = item + while (currentParent) { + out.push(currentParent) + currentParent = currentParent.parent + } + return out +} + +// 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])] + for (let comboSize = 2; comboSize <= array.length; comboSize++) { + const previous = combos[combos.length - 1] + 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]) + }) + const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], []) + const uniqueComboStrings = new Set() + const uniqueCombos = flatCombos.map(sortBy).filter(x => { + if (uniqueComboStrings.has(x.join())) { + return false + } else { + uniqueComboStrings.add(x.join()) + return true + } + }) + combos.push(uniqueCombos) + } + return combos.reduce((acc, x) => [...acc, ...x], []) +} + +/** + * Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) + * selector. + * + * "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal + * purposes + * + * @param {Object} components - object containing all components definitions + * + * @returns {Function} + * @param {Object} rule - rule in question to convert to CSS selector + * @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in + * component definition and use selector + * @param {boolean} isParent - (mostly) internal argument used when recursing + * + * @returns {String} CSS selector (or path) + */ +export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { + if (!rule && !isParent) return null + const component = components[rule.component] + const { states = {}, variants = {}, selector, outOfTreeSelector } = component + + const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) + + const applicableVariantName = (rule.variant || 'normal') + let applicableVariant = '' + if (applicableVariantName !== 'normal') { + applicableVariant = variants[applicableVariantName] + } else { + applicableVariant = variants?.normal ?? '' + } + + let realSelector + if (selector === ':root') { + realSelector = '' + } else if (isParent) { + realSelector = selector + } else { + if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector + else realSelector = selector + } + + const selectors = [realSelector, applicableVariant, ...applicableStates] + .sort((a, b) => { + if (a.startsWith(':')) return 1 + if (/^[a-z]/.exec(a)) return -1 + else return 0 + }) + .join('') + + if (rule.parent) { + return (genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim() + } + return selectors.trim() +} + +/** + * Check if combination matches + * + * @param {Object} criteria - criteria to match against + * @param {Object} subject - rule/combination to check match + * @param {boolean} strict - strict checking: + * By default every variant and state inherits from "normal" state/variant + * so when checking if combination matches, it WILL match against "normal" + * state/variant. In strict mode inheritance is ignored an "normal" does + * not match + */ +export const combinationsMatch = (criteria, subject, strict) => { + if (criteria.component !== subject.component) return false + + // All variants inherit from normal + if (subject.variant !== 'normal' || strict) { + if (criteria.variant !== subject.variant) return false + } + + // Subject states > 1 essentially means state is "normal" and therefore matches + if (subject.state.length > 1 || strict) { + const subjectStatesSet = new Set(subject.state) + const criteriaStatesSet = new Set(criteria.state) + + const setsAreEqual = + [...criteriaStatesSet].every(state => subjectStatesSet.has(state)) && + [...subjectStatesSet].every(state => criteriaStatesSet.has(state)) + + if (!setsAreEqual) return false + } + return true +} + +/** + * Search for rule that matches `criteria` in set of rules + * meant to be used in a ruleset.filter() function + * + * @param {Object} criteria - criteria to search for + * @param {boolean} strict - whether search strictly or not (see combinationsMatch) + * + * @return function that returns true/false if subject matches + */ +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 + + if (criteria.parent !== undefined && criteria.parent !== null) { + if (!subject.parent && !strict) return true + const pathCriteria = unroll(criteria) + const pathSubject = unroll(subject) + if (pathCriteria.length < pathSubject.length) return false + + // Search: .a .b .c + // Matches: .a .b .c; .b .c; .c; .z .a .b .c + // Does not match .a .b .c .d, .a .b .e + for (let i = 0; i < pathCriteria.length; i++) { + const criteriaParent = pathCriteria[i] + const subjectParent = pathSubject[i] + if (!subjectParent) return true + if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false + } + } + return true +} + +// Pre-fills 'normal' state/variant if missing +export const normalizeCombination = rule => { + rule.variant = rule.variant ?? 'normal' + rule.state = [...new Set(['normal', ...(rule.state || [])])] +} diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js @@ -0,0 +1,2 @@ +export const sampleRules = [ +] diff --git a/src/services/theme_data/theme2_keys.js b/src/services/theme_data/theme2_keys.js @@ -0,0 +1,177 @@ +export default [ + 'bg', + 'wallpaper', + 'fg', + 'text', + 'underlay', + 'link', + 'accent', + 'faint', + 'faintLink', + 'postFaintLink', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange', + + 'profileBg', + 'profileTint', + + 'highlight', + 'highlightLightText', + 'highlightPostLink', + 'highlightFaintText', + 'highlightFaintLink', + 'highlightPostFaintLink', + 'highlightText', + 'highlightLink', + 'highlightIcon', + + 'popover', + 'popoverLightText', + 'popoverPostLink', + 'popoverFaintText', + 'popoverFaintLink', + 'popoverPostFaintLink', + 'popoverText', + 'popoverLink', + 'popoverIcon', + + 'selectedPost', + 'selectedPostFaintText', + 'selectedPostLightText', + 'selectedPostPostLink', + 'selectedPostFaintLink', + 'selectedPostText', + 'selectedPostLink', + 'selectedPostIcon', + + 'selectedMenu', + 'selectedMenuLightText', + 'selectedMenuFaintText', + 'selectedMenuFaintLink', + 'selectedMenuText', + 'selectedMenuLink', + 'selectedMenuIcon', + + 'selectedMenuPopover', + 'selectedMenuPopoverLightText', + 'selectedMenuPopoverFaintText', + 'selectedMenuPopoverFaintLink', + 'selectedMenuPopoverText', + 'selectedMenuPopoverLink', + 'selectedMenuPopoverIcon', + + 'lightText', + + 'postLink', + + 'postGreentext', + + 'postCyantext', + + 'border', + + 'poll', + 'pollText', + + 'icon', + + // Foreground, + 'fgText', + 'fgLink', + + // Panel header, + 'panel', + 'panelText', + 'panelFaint', + 'panelLink', + + // Top bar, + 'topBar', + 'topBarText', + 'topBarLink', + + // Tabs, + 'tab', + 'tabText', + 'tabActiveText', + + // Buttons, + 'btn', + 'btnText', + 'btnPanelText', + 'btnTopBarText', + + // Buttons: pressed, + 'btnPressed', + 'btnPressedText', + 'btnPressedPanel', + 'btnPressedPanelText', + 'btnPressedTopBar', + 'btnPressedTopBarText', + + // Buttons: toggled, + 'btnToggled', + 'btnToggledText', + 'btnToggledPanelText', + 'btnToggledTopBarText', + + // Buttons: disabled, + 'btnDisabled', + 'btnDisabledText', + 'btnDisabledPanelText', + 'btnDisabledTopBarText', + + // Input fields, + 'input', + 'inputText', + 'inputPanelText', + 'inputTopbarText', + + 'alertError', + 'alertErrorText', + 'alertErrorPanelText', + + 'alertWarning', + 'alertWarningText', + 'alertWarningPanelText', + + 'alertSuccess', + 'alertSuccessText', + 'alertSuccessPanelText', + + 'alertNeutral', + 'alertNeutralText', + 'alertNeutralPanelText', + + 'alertPopupError', + 'alertPopupErrorText', + + 'alertPopupWarning', + 'alertPopupWarningText', + + 'alertPopupSuccess', + 'alertPopupSuccessText', + + 'alertPopupNeutral', + 'alertPopupNeutralText', + + 'badgeNeutral', + 'badgeNeutralText', + + 'badgeNotification', + 'badgeNotificationText', + + 'chatBg', + + 'chatMessageIncomingBg', + 'chatMessageIncomingText', + 'chatMessageIncomingLink', + 'chatMessageIncomingBorder', + 'chatMessageOutgoingBg', + 'chatMessageOutgoingText', + 'chatMessageOutgoingLink', + 'chatMessageOutgoingBorder' +] diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js @@ -0,0 +1,539 @@ +import { convert } from 'chromatism' +import allKeys from './theme2_keys' + +// keys that are meant to be used globally, i.e. what's the rest of the theme is based upon. +export const basePaletteKeys = new Set([ + 'bg', + 'fg', + 'text', + 'link', + 'accent', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange', + + 'wallpaper' +]) + +export const fontsKeys = new Set([ + 'interface', + 'input', + 'post', + 'postCode' +]) + +export const opacityKeys = new Set([ + 'alert', + 'alertPopup', + 'bg', + 'border', + 'btn', + 'faint', + 'input', + 'panel', + 'popover', + 'profileTint', + 'underlay' +]) + +export const shadowsKeys = new Set([ + 'panel', + 'topBar', + 'popup', + 'avatar', + 'avatarStatus', + 'panelHeader', + 'button', + 'buttonHover', + 'buttonPressed', + 'input' +]) + +export const radiiKeys = new Set([ + 'btn', + 'input', + 'checkbox', + 'panel', + 'avatar', + 'avatarAlt', + 'tooltip', + 'attachment', + 'chatMessage' +]) + +// Keys that are not available in editor and never meant to be edited +export const hiddenKeys = new Set([ + 'profileBg', + 'profileTint' +]) + +export const extendedBasePrefixes = [ + 'border', + 'icon', + 'highlight', + 'lightText', + + 'popover', + + 'panel', + 'topBar', + 'tab', + 'btn', + 'input', + 'selectedMenu', + + 'alert', + 'alertPopup', + 'badge', + + 'post', + 'selectedPost', // wrong nomenclature + 'poll', + + 'chatBg', + 'chatMessage' +] +export const nonComponentPrefixes = new Set([ + 'border', + 'icon', + 'highlight', + 'lightText', + 'chatBg' +]) + +export const extendedBaseKeys = Object.fromEntries( + extendedBasePrefixes.map(prefix => [ + prefix, + 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 temporaryColors = {} + +export const convertTheme2To3 = (data) => { + data.colors.accent = data.colors.accent || data.colors.link + data.colors.link = data.colors.link || data.colors.accent + const generateRoot = () => { + const directives = {} + basePaletteKeys.forEach(key => { directives['--' + key] = 'color | ' + convert(data.colors[key]).hex }) + return { + component: 'Root', + directives + } + } + + const convertOpacity = () => { + const newRules = [] + 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' } + + switch (key) { + case 'alert': + rule.component = 'Alert' + break + case 'alertPopup': + rule.component = 'Alert' + rule.parent = { component: 'Popover' } + break + case 'bg': + rule.component = 'Panel' + break + case 'border': + rule.component = 'Border' + break + case 'btn': + rule.component = 'Button' + break + case 'faint': + rule.component = 'Text' + rule.state = ['faint'] + break + case 'input': + rule.component = 'Input' + break + case 'panel': + rule.component = 'PanelHeader' + break + case 'popover': + rule.component = 'Popover' + break + case 'profileTint': + return null + case 'underlay': + rule.component = 'Underlay' + break + } + + switch (key) { + case 'alert': + case 'alertPopup': + case 'bg': + case 'btn': + case 'input': + case 'panel': + case 'popover': + case 'underlay': + rule.directives = { opacity: originalOpacity } + break + case 'faint': + case 'border': + rule.directives = { textOpacity: originalOpacity } + break + } + + newRules.push(rule) + + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + newRules.push({ ...rule, component: 'Tab', state: ['active'], directives: { opacity: 0 } }) + } + if (rule.component === 'Panel') { + newRules.push({ ...rule, component: 'Post' }) + } + }) + return newRules + } + + const convertRadii = () => { + const newRules = [] + 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' } + + switch (key) { + case 'btn': + rule.component = 'Button' + break + case 'tab': + rule.component = 'Tab' + break + case 'input': + rule.component = 'Input' + break + case 'checkbox': + rule.component = 'Input' + rule.variant = 'checkbox' + break + case 'panel': + rule.component = 'Panel' + break + case 'avatar': + rule.component = 'Avatar' + break + case 'avatarAlt': + rule.component = 'Avatar' + rule.variant = 'compact' + break + case 'tooltip': + rule.component = 'Popover' + break + case 'attachment': + rule.component = 'Attachment' + break + case 'ChatMessage': + rule.component = 'Button' + break + } + rule.directives = { + roundness: originalRadius + } + newRules.push(rule) + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + } + }) + return newRules + } + + const convertFonts = () => { + const newRules = [] + Object.keys(data.fonts || {}).forEach(key => { + if (!fontsKeys.has(key)) return + if (!data.fonts[key]) return + const originalFont = data.fonts[key].family + const rule = { source: '2to3' } + + switch (key) { + case 'interface': + case 'postCode': + rule.component = 'Root' + break + case 'input': + rule.component = 'Input' + break + case 'post': + rule.component = 'RichContent' + break + } + switch (key) { + case 'interface': + case 'input': + case 'post': + rule.directives = { '--font': 'generic | ' + originalFont } + break + case 'postCode': + rule.directives = { '--monoFont': 'generic | ' + originalFont } + newRules.push({ ...rule, component: 'RichContent' }) + break + } + newRules.push(rule) + }) + return newRules + } + const convertShadows = () => { + const newRules = [] + Object.keys(data.shadows || {}).forEach(key => { + if (!shadowsKeys.has(key)) return + const originalShadow = data.shadows[key] + const rule = { source: '2to3' } + + switch (key) { + case 'panel': + rule.component = 'Panel' + break + case 'topBar': + rule.component = 'TopBar' + break + case 'popup': + rule.component = 'Popover' + break + case 'avatar': + rule.component = 'Avatar' + break + case 'avatarStatus': + rule.component = 'Avatar' + rule.parent = { component: 'Post' } + break + case 'panelHeader': + rule.component = 'PanelHeader' + break + case 'button': + rule.component = 'Button' + break + case 'buttonHover': + rule.component = 'Button' + rule.state = ['hover'] + break + case 'buttonPressed': + rule.component = 'Button' + rule.state = ['pressed'] + break + case 'input': + rule.component = 'Input' + break + } + rule.directives = { + shadow: originalShadow + } + newRules.push(rule) + if (key === 'topBar') { + newRules.push({ ...rule, component: 'PanelHeader', parent: { component: 'MobileDrawer' } }) + } + if (key === 'avatarStatus') { + newRules.push({ ...rule, parent: { component: 'Notification' } }) + } + if (key === 'buttonPressed') { + newRules.push({ ...rule, state: ['toggled'] }) + newRules.push({ ...rule, state: ['toggled', 'focus'] }) + newRules.push({ ...rule, state: ['pressed', 'focus'] }) + } + if (key === 'buttonHover') { + newRules.push({ ...rule, state: ['toggled', 'hover'] }) + newRules.push({ ...rule, state: ['pressed', 'hover'] }) + newRules.push({ ...rule, state: ['toggled', 'focus', 'hover'] }) + newRules.push({ ...rule, state: ['pressed', 'focus', 'hover'] }) + } + + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + } + }) + 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] + newRule.directives.textAuto = 'no-auto' + variantArray = parts.slice(0, -1) + break + default: + newRule = { ...rule, directives: {} } + newRule.directives.background = data.colors[key] + variantArray = parts + break + } + + 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 '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 + } + } + + 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' }] + } 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], []) + + return [generateRoot(), ...convertShadows(), ...convertRadii(), ...convertOpacity(), ...convertFonts(), ...flatExtRules] +} diff --git a/src/services/theme_data/theme3_slot_functions.js b/src/services/theme_data/theme3_slot_functions.js @@ -0,0 +1,103 @@ +import { convert, brightness } from 'chromatism' +import { alphaBlend, 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()) + + const func = functions[funcName] + if (args.length < func.argsNeeded) { + throw new Error(`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`) + } + return func.exec(args, { findColor, findShadow }, { dynamicVars, staticVars }) +} + +export const colorFunctions = { + alpha: { + argsNeeded: 2, + exec: (args, { findColor }, { dynamicVars, staticVars }) => { + const [color, amountArg] = args + + const colorArg = convert(findColor(color, { dynamicVars, staticVars })).rgb + const amount = Number(amountArg) + return { ...colorArg, a: amount } + } + }, + textColor: { + argsNeeded: 2, + 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 + + return getTextColor(background, foreground, preserve === 'preserve') + } + }, + blend: { + argsNeeded: 3, + 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 amount = Number(amountArg) + + return alphaBlend(background, amount, foreground) + } + }, + mod: { + argsNeeded: 2, + exec: (args, { findColor }, { dynamicVars, staticVars }) => { + const [colorArg, amountArg] = args + + const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb + const amount = Number(amountArg) + + const effectiveBackground = dynamicVars.lowerLevelBackground + 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, + exec: (args, { findColor }) => { + const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args + + const width = Number(widthArg) + const isInset = inset === 'inset' + + const targetShadow = { + x: 0, + y: 0, + blur: 0, + spread: 0, + color, + alpha: Number(alpha), + inset: isInset + } + + side.split('-').forEach((position) => { + switch (position) { + case 'left': + targetShadow.x = width * (inset ? 1 : -1) + break + case 'right': + targetShadow.x = -1 * width * (inset ? 1 : -1) + break + case 'top': + targetShadow.y = width * (inset ? 1 : -1) + break + case 'bottom': + targetShadow.y = -1 * width * (inset ? 1 : -1) + break + } + }) + return [targetShadow] + } + } +} diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js @@ -1,5 +1,5 @@ import { convert, brightness, contrastRatio } from 'chromatism' -import { alphaBlendLayers, getTextColor, relativeLuminance } 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' /* @@ -117,7 +117,6 @@ export const topoSort = ( // Put it into the output list output.push(node) } else if (grays.has(node)) { - console.debug('Cyclic depenency in topoSort, ignoring') output.push(node) } else if (blacks.has(node)) { // do nothing @@ -407,3 +406,347 @@ export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ } } }, { colors: {}, opacity: {} }) + +export const composePreset = (colors, radii, shadows, fonts) => { + return { + rules: { + ...shadows.rules, + ...colors.rules, + ...radii.rules, + ...fonts.rules + }, + theme: { + ...shadows.theme, + ...colors.theme, + ...radii.theme, + ...fonts.theme + } + } +} + +export const generatePreset = (input) => { + const colors = generateColors(input) + return composePreset( + colors, + generateRadii(input), + generateShadows(input, colors.theme.colors, colors.mod), + generateFonts(input) + ) +} + +export const getCssShadow = (input, usesDropShadow) => { + if (input.length === 0) { + return 'none' + } + + 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(', ') +} + +const getCssShadowFilter = (input) => { + if (input.length === 0) { + 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(' ') +} + +export const generateColors = (themeData) => { + const sourceColors = !themeData.themeEngineVersion + ? colors2to3(themeData.colors || themeData) + : themeData.colors || themeData + + const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}) + + 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: {} }) + return { + rules: { + colors: Object.entries(htmlColors.complete) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}: ${v}`) + .join(';') + }, + theme: { + colors: htmlColors.solid, + opacity + } + } +} + +export const generateRadii = (input) => { + let inputRadii = input.radii || {} + // v1 -> v2 + if (typeof input.btnRadius !== 'undefined') { + inputRadii = Object + .entries(input) + .filter(([k, v]) => k.endsWith('Radius')) + .reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {}) + } + const radii = Object.entries(inputRadii).filter(([k, 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(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';') + }, + theme: { + radii + } + } +} + +export const generateFonts = (input) => { + const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => { + acc[k] = Object.entries(v).filter(([k, 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) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}Font: ${v.family}`).join(';') + }, + theme: { + fonts + } + } +} + +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) +const buttonInsetFakeBorders = [border(true, false), border(false, true)] +const inputInsetFakeBorders = [border(true, true), border(false, false)] +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--faint', + 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 + }], + avatarStatus: [], + panelHeader: [], + 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 + }] +} +export const generateShadows = (input, colors) => { + // TODO this is a small hack for `mod` to work with shadows + // this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element + const hackContextDict = { + button: 'btn', + panel: 'bg', + top: 'topBar', + popup: 'popover', + avatar: 'bg', + panelHeader: 'panel', + 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 })) + ]) + ) + const inputShadows = cleanInputShadows && !input.themeEngineVersion + ? shadows2to3(cleanInputShadows, input.opacity) + : cleanInputShadows || {} + const shadows = Object.entries({ + ...DEFAULT_SHADOWS, + ...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 mod = isLightOnDark ? 1 : -1 + 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(';') + }, + theme: { + shadows + } + } +} + +/** + * This handles compatibility issues when importing v2 theme's shadows to current format + * + * 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 } + }, {}) +} + +export const colors2to3 = (colors) => { + return Object.entries(colors).reduce((acc, [slotName, color]) => { + const btnPositions = ['', 'Panel', 'TopBar'] + switch (slotName) { + case 'lightBg': + return { ...acc, highlight: color } + case 'btnText': + return { + ...acc, + ...btnPositions + .reduce( + (statePositionAcc, position) => + ({ ...statePositionAcc, ['btn' + position + 'Text']: color }) + , {} + ) + } + default: + return { ...acc, [slotName]: color } + } + }, {}) +} diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js @@ -0,0 +1,513 @@ +import { convert, brightness } from 'chromatism' +import sum from 'hash-sum' +import { flattenDeep, sortBy } from 'lodash' +import { + alphaBlend, + getTextColor, + rgba2css, + mixrgb, + relativeLuminance +} from '../color_convert/color_convert.js' + +import { + colorFunctions, + shadowFunctions, + process +} from './theme3_slot_functions.js' + +import { + unroll, + getAllPossibleCombinations, + genericRuleToSelector, + normalizeCombination, + findRules +} from './iss_utils.js' +import { parseCssShadow } from './css_utils.js' + +// Ensuring the order of components +const components = { + Root: null, + Text: null, + FunText: null, + Link: null, + Icon: null, + Border: null, + Panel: null, + Chat: null, + ChatMessage: null +} + +const findShadow = (shadows, { dynamicVars, staticVars }) => { + return (shadows || []).map(shadow => { + let targetShadow + if (typeof shadow === 'string') { + if (shadow.startsWith('$')) { + targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars }) + } else if (shadow.startsWith('--')) { + const [variable] = shadow.split(/,/g).map(str => str.trim()) // discarding modifier since it's not supported + const variableSlot = variable.substring(2) + return findShadow(staticVars[variableSlot], { dynamicVars, staticVars }) + } else { + targetShadow = parseCssShadow(shadow) + } + } else { + targetShadow = shadow + } + + const shadowArray = Array.isArray(targetShadow) ? targetShadow : [targetShadow] + return shadowArray.map(s => ({ + ...s, + color: findColor(s.color, { dynamicVars, staticVars }) + })) + }) +} + +const findColor = (color, { dynamicVars, staticVars }) => { + if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color + let targetColor = null + if (color.startsWith('--')) { + const [variable, modifier] = color.split(/,/g).map(str => str.trim()) + const variableSlot = variable.substring(2) + if (variableSlot === 'stack') { + const { r, g, b } = dynamicVars.stacked + targetColor = { r, g, b } + } else if (variableSlot.startsWith('parent')) { + if (variableSlot === 'parent') { + const { r, g, b } = dynamicVars.lowerLevelBackground + targetColor = { r, g, b } + } else { + const virtualSlot = variableSlot.replace(/^parent/, '') + targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb + } + } else { + switch (variableSlot) { + case 'inheritedBackground': + targetColor = convert(dynamicVars.inheritedBackground).rgb + break + case 'background': + targetColor = convert(dynamicVars.background).rgb + break + default: + targetColor = convert(staticVars[variableSlot]).rgb + } + } + + if (modifier) { + 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 + } + } + + if (color.startsWith('$')) { + try { + targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars }) + } catch (e) { + console.error('Failure executing color function', e) + targetColor = '#FF00FF' + } + } + // Color references other color + return targetColor +} + +const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => { + const opacity = directives.textOpacity + const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb + const textColor = convert(findColor(intendedTextColor, { dynamicVars, staticVars })).rgb + if (opacity === null || opacity === undefined || opacity >= 1) { + return convert(textColor).hex + } + if (opacity === 0) { + return convert(backgroundColor).hex + } + const opacityMode = directives.textOpacityMode + switch (opacityMode) { + case 'fake': + return convert(alphaBlend(textColor, opacity, backgroundColor)).hex + case 'mixrgb': + return convert(mixrgb(backgroundColor, textColor)).hex + default: + return rgba2css({ a: opacity, ...textColor }) + } +} + +// Loading all style.js[on] files dynamically +const componentsContext = require.context('src', true, /\.style.js(on)?$/) +componentsContext.keys().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!`) + } + components[component.name] = component +}) + +const engineChecksum = sum(components) + +const ruleToSelector = genericRuleToSelector(components) + +export const getEngineChecksum = () => engineChecksum + +/** + * Initializes and compiles the theme according to the ruleset + * + * @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to + * component default rulesets + * @param {string} ultimateBackgroundColor - Color that will be the "final" background for + * calculating contrast ratios and making text automatically accessible. Really used for cases when + * stuff is transparent. + * @param {boolean} debug - print out debug information in console, mostly just performance stuff + * @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to + * generatate theme previews and such that need to be compiled faster and don't require a lot of other + * components present in "normal" mode + * @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme + * previews since states are the biggest factor for compilation time and are completely unnecessary + * when previewing multiple themes at same time + * @param {string} rootComponentName - [UNTESTED] which component to start from, meant for previewing a + * part of the theme (i.e. just the button) for themes 3 editor. + */ +export const init = ({ + inputRuleset, + ultimateBackgroundColor, + debug = false, + liteMode = false, + onlyNormalState = false, + rootComponentName = 'Root' +}) => { + if (!inputRuleset) throw new Error('Ruleset is null or undefined!') + const staticVars = {} + const stacked = {} + const computed = {} + + const rulesetUnsorted = [ + ...Object.values(components) + .map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r, source: 'Built-in' }))) + .reduce((acc, arr) => [...acc, ...arr], []), + ...inputRuleset + ].map(rule => { + normalizeCombination(rule) + let currentParent = rule.parent + while (currentParent) { + normalizeCombination(currentParent) + currentParent = currentParent.parent + } + + return rule + }) + + const ruleset = rulesetUnsorted + .map((data, index) => ({ data, index })) + .sort(({ data: a, index: ai }, { data: b, index: bi }) => { + const parentsA = unroll(a).length + const parentsB = unroll(b).length + + if (parentsA === parentsB) { + if (a.component === 'Text') return -1 + if (b.component === 'Text') return 1 + return ai - bi + } + if (parentsA === 0 && parentsB !== 0) return -1 + if (parentsB === 0 && parentsA !== 0) return 1 + return parentsA - parentsB + }) + .map(({ data }) => data) + + const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name)) + + const processCombination = (combination) => { + const selector = ruleToSelector(combination, true) + const cssSelector = ruleToSelector(combination) + + const parentSelector = selector.split(/ /g).slice(0, -1).join(' ') + const soloSelector = selector.split(/ /g).slice(-1)[0] + + const lowerLevelSelector = parentSelector + const lowerLevelBackground = computed[lowerLevelSelector]?.background + const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives + const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw + + const dynamicVars = computed[selector] || { + lowerLevelBackground, + lowerLevelVirtualDirectives, + 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 computedRule = { + ...combination, + directives: computedDirectives + } + + computed[selector] = computed[selector] || {} + computed[selector].computedRule = computedRule + computed[selector].dynamicVars = dynamicVars + + if (virtualComponents.has(combination.component)) { + const virtualName = [ + '--', + 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()) + ].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 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 + } + + const newTextRule = { + ...computedRule, + directives: { + ...computedRule.directives, + textColor: inheritedTextColor, + textAuto: inheritedTextAuto ?? 'preserve', + textOpacity: inheritedTextOpacity, + 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 || {} + + // Storing color data in lower layer to use as custom css properties + virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars) + virtualDirectivesRaw[virtualName] = textColor + + computed[lowerLevelSelector].virtualDirectives = virtualDirectives + computed[lowerLevelSelector].virtualDirectivesRaw = virtualDirectivesRaw + + return { + dynamicVars, + selector: cssSelector.split(/ /g).slice(0, -1).join(' '), + ...combination, + directives: {}, + virtualDirectives: { + [virtualName]: getTextColorAlpha(newTextRule.directives, textColor, dynamicVars) + }, + virtualDirectivesRaw: { + [virtualName]: textColor + } + } + } else { + computed[selector] = computed[selector] || {} + + // TODO: DEFAULT TEXT COLOR + const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb + + if (computedDirectives.background) { + let inheritRule = null + const variantRules = ruleset.filter( + findRules({ + component: combination.component, + variant: combination.variant, + 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 lastNormalRule = normalRules[normalRules.length - 1] + inheritRule = lastNormalRule + } + + 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 + + if (!stacked[selector]) { + let blend + const alpha = computedDirectives.opacity ?? 1 + if (alpha >= 1) { + blend = rgb + } else if (alpha <= 0) { + blend = lowerLevelStackedBackground + } else { + blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground) + } + stacked[selector] = blend + computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 } + } + } + + if (computedDirectives.shadow) { + 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 } + } + + dynamicVars.stacked = stacked[selector] + dynamicVars.background = computed[selector].background + + const dynamicSlots = Object.entries(computedDirectives).filter(([k, v]) => k.startsWith('--')) + + dynamicSlots.forEach(([k, v]) => { + const [type, ...value] = v.split('|').map(x => x.trim()) // woah, Extreme! + switch (type) { + case 'color': { + const color = findColor(value[0], { dynamicVars, staticVars }) + dynamicVars[k] = color + if (combination.component === 'Root') { + staticVars[k.substring(2)] = color + } + break + } + case 'shadow': { + const shadow = value + dynamicVars[k] = shadow + if (combination.component === 'Root') { + staticVars[k.substring(2)] = shadow + } + break + } + case 'generic': { + dynamicVars[k] = value + if (combination.component === 'Root') { + staticVars[k.substring(2)] = value + } + break + } + } + }) + + const rule = { + dynamicVars, + selector: cssSelector, + ...combination, + directives: computedDirectives + } + + return rule + } + } + + const processInnerComponent = (component, parent) => { + const combinations = [] + const { + states: originalStates = {}, + variants: originalVariants = {} + } = component + + const validInnerComponents = ( + liteMode + ? (component.validInnerComponentsLite || component.validInnerComponents) + : component.validInnerComponents + ) || [] + + // Normalizing states and variants to always include "normal" + const states = { normal: '', ...originalStates } + const variants = { normal: '', ...originalVariants } + 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!`) + 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 + ? [ + ['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 + }) + ] + + const stateVariantCombination = Object.keys(variants).map(variant => { + return stateCombinations.map(state => ({ variant, state })) + }).reduce((acc, x) => [...acc, ...x], []) + + stateVariantCombination.forEach(combination => { + combination.component = component.name + combination.lazy = component.lazy || parent?.lazy + combination.parent = parent + if (combination.state.indexOf('hover') >= 0) { + combination.lazy = true + } + + combinations.push(combination) + + innerComponents.forEach(innerComponent => { + combinations.push(...processInnerComponent(innerComponent, combination)) + }) + }) + + return combinations + } + + const t0 = performance.now() + 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 t2 = performance.now() + if (debug) { + console.debug('Eager processing took ' + (t2 - t1) + ' ms') + } + + return { + lazy: result.filter(x => typeof x === 'function'), + eager: result.filter(x => typeof x !== 'function'), + staticVars, + engineChecksum + } +} diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -24,6 +24,7 @@ const fetchAndUpdate = ({ showImmediately = false, userId = false, listId = false, + statusId = false, tag = false, until, since @@ -47,6 +48,7 @@ const fetchAndUpdate = ({ args.userId = userId args.listId = listId + args.statusId = statusId args.tag = tag args.withMuted = !hideMutedPosts if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) { @@ -78,15 +80,15 @@ const fetchAndUpdate = ({ }) } -const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, tag = false }) => { +const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, statusId = 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 - fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, tag }) + fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, statusId, tag }) const boundFetchAndUpdate = () => - fetchAndUpdate({ timeline, credentials, store, userId, listId, tag }) + fetchAndUpdate({ timeline, credentials, store, userId, listId, statusId, tag }) return promiseInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { diff --git a/src/sw.js b/src/sw.js @@ -13,9 +13,10 @@ const i18n = createI18n({ messages }) -function isEnabled () { - return localForage.getItem('vuex-lz') - .then(data => data.config.webPushNotifications) +const state = { + lastFocused: null, + notificationIds: new Set(), + allowedNotificationTypes: null } function getWindowClients () { @@ -23,17 +24,48 @@ function getWindowClients () { .then((clientList) => clientList.filter(({ type }) => type === 'window')) } -const setLocale = async () => { - const state = await localForage.getItem('vuex-lz') - const locale = state.config.interfaceLanguage || 'en' +const setSettings = async () => { + const vuexState = await localForage.getItem('vuex-lz') + const locale = vuexState.config.interfaceLanguage || 'en' i18n.locale = locale + const notificationsNativeArray = Object.entries(vuexState.config.notificationNative) + state.webPushAlwaysShowNotifications = vuexState.config.webPushAlwaysShowNotifications + + state.allowedNotificationTypes = new Set( + notificationsNativeArray + .filter(([k, v]) => v) + .map(([k]) => { + switch (k) { + case 'mentions': + return 'mention' + case 'statuses': + return 'status' + case 'likes': + return 'like' + case 'repeats': + return 'repeat' + case 'emojiReactions': + return 'pleroma:emoji_reaction' + case 'reports': + return 'pleroma:report' + case 'followRequest': + return 'follow_request' + case 'follows': + return 'follow' + case 'polls': + return 'poll' + default: + return k + } + }) + ) } -const maybeShowNotification = async (event) => { - const enabled = await isEnabled() +const showPushNotification = async (event) => { const activeClients = await getWindowClients() - await setLocale() - if (enabled && (activeClients.length === 0)) { + await setSettings() + // Only show push notifications if all tabs/windows are closed + if (state.webPushAlwaysShowNotifications || activeClients.length === 0) { const data = event.data.json() const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}` @@ -43,13 +75,48 @@ const maybeShowNotification = async (event) => { const res = prepareNotificationObject(parsedNotification, i18n) - self.registration.showNotification(res.title, res) + if (state.webPushAlwaysShowNotifications || state.allowedNotificationTypes.has(parsedNotification.type)) { + return self.registration.showNotification(res.title, res) + } } + return Promise.resolve() } self.addEventListener('push', async (event) => { if (event.data) { - event.waitUntil(maybeShowNotification(event)) + // Supposedly, we HAVE to return a promise inside waitUntil otherwise it will + // show (extra) notification that website is updated in background + event.waitUntil(showPushNotification(event)) + } +}) + +self.addEventListener('message', async (event) => { + await setSettings() + const { type, content } = event.data + + if (type === 'desktopNotification') { + const { title, ...rest } = content + const { tag, type } = rest + if (state.notificationIds.has(tag)) return + state.notificationIds.add(tag) + setTimeout(() => state.notificationIds.delete(tag), 10000) + if (state.allowedNotificationTypes.has(type)) { + self.registration.showNotification(title, rest) + } + } + + if (type === 'desktopNotificationClose') { + const { id, all } = content + const search = all ? null : { tag: id } + const notifications = await self.registration.getNotifications(search) + notifications.forEach(n => n.close()) + } + + if (type === 'updateFocus') { + state.lastFocused = event.source.id + + const notifications = await self.registration.getNotifications() + notifications.forEach(n => n.close()) } }) @@ -59,7 +126,14 @@ self.addEventListener('notificationclick', (event) => { event.waitUntil(getWindowClients().then((list) => { for (let i = 0; i < list.length; i++) { const client = list[i] - if (client.url === '/' && 'focus' in client) { return client.focus() } + client.postMessage({ type: 'notificationClicked', id: event.notification.tag }) + } + + for (let i = 0; i < list.length; i++) { + const client = list[i] + if (state.lastFocused === null || client.id === state.lastFocused) { + if ('focus' in client) return client.focus() + } } if (clients.openWindow) return clients.openWindow('/') diff --git a/test/e2e/nightwatch.conf.js b/test/e2e/nightwatch.conf.js @@ -9,7 +9,7 @@ module.exports = { selenium: { start_process: true, - server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar', + server_path: require('selenium-server').path, host: '127.0.0.1', port: 4444, cli_args: { diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js @@ -77,24 +77,6 @@ describe('Statuses module', () => { expect(state.timelines.public.newStatusCount).to.equal(0) }) - it('removes statuses by tag on deletion', () => { - const state = defaultState() - const status = makeMockStatus({ id: '1' }) - const otherStatus = makeMockStatus({ id: '3' }) - status.uri = 'xxx' - const deletion = makeMockStatus({ id: '2', type: 'deletion' }) - deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' - deletion.uri = 'xxx' - - mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' }) - mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' }) - - expect(state.allStatuses).to.eql([otherStatus]) - expect(state.timelines.public.statuses).to.eql([otherStatus]) - expect(state.timelines.public.visibleStatuses).to.eql([otherStatus]) - expect(state.timelines.public.maxId).to.eql('3') - }) - it('does not update the maxId when the noIdUpdate flag is set', () => { const state = defaultState() const status = makeMockStatus({ id: '1' }) @@ -315,62 +297,4 @@ describe('Statuses module', () => { expect(state.timelines.user.userId).to.eql(123) }) }) - - describe('notifications', () => { - it('removes a notification when the notice gets removed', () => { - const user = { id: '1' } - const state = defaultState() - const status = makeMockStatus({ id: '1' }) - const otherStatus = makeMockStatus({ id: '3' }) - const mentionedStatus = makeMockStatus({ id: '2' }) - mentionedStatus.attentions = [user] - mentionedStatus.uri = 'xxx' - otherStatus.attentions = [user] - - const deletion = makeMockStatus({ id: '4', type: 'deletion' }) - deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' - deletion.uri = 'xxx' - const newNotificationSideEffects = () => {} - mutations.addNewStatuses(state, { statuses: [status, otherStatus], user }) - mutations.addNewNotifications( - state, - { - notifications: [{ - from_profile: { id: '2' }, - id: '998', - type: 'mention', - status: otherStatus, - action: otherStatus, - seen: false - }], - newNotificationSideEffects - }) - - expect(state.notifications.data.length).to.eql(1) - mutations.addNewNotifications( - state, - { - notifications: [{ - from_profile: { id: '2' }, - id: '999', - type: 'mention', - status: mentionedStatus, - action: mentionedStatus, - seen: false - }], - newNotificationSideEffects - }) - - mutations.addNewStatuses(state, { statuses: [mentionedStatus], user }) - expect(state.allStatuses.length).to.eql(3) - expect(state.notifications.data.length).to.eql(2) - expect(state.notifications.data[1].status).to.eql(mentionedStatus) - expect(state.notifications.data[1].action).to.eql(mentionedStatus) - expect(state.notifications.data[1].type).to.eql('mention') - - mutations.addNewStatuses(state, { statuses: [deletion], user }) - expect(state.allStatuses.length).to.eql(2) - expect(state.notifications.data.length).to.eql(1) - }) - }) }) diff --git a/test/unit/specs/services/notification_utils/notification_utils.spec.js b/test/unit/specs/services/notification_utils/notification_utils.spec.js @@ -5,28 +5,28 @@ describe('NotificationUtils', () => { it('should return sorted notifications with configured types', () => { const store = { state: { - statuses: { - notifications: { - data: [ - { - id: 1, - action: { id: '1' }, - type: 'like' - }, - { - id: 2, - action: { id: '2' }, - type: 'mention' - }, - { - id: 3, - action: { id: '3' }, - type: 'repeat' - } - ] - } - }, - config: { + notifications: { + data: [ + { + id: 1, + action: { id: '1' }, + type: 'like' + }, + { + id: 2, + action: { id: '2' }, + type: 'mention' + }, + { + id: 3, + action: { id: '3' }, + type: 'repeat' + } + ] + } + }, + getters: { + mergedConfig: { notificationVisibility: { likes: true, repeats: true, @@ -55,23 +55,23 @@ describe('NotificationUtils', () => { it('should return only notifications not marked as seen', () => { const store = { state: { - statuses: { - notifications: { - data: [ - { - action: { id: '1' }, - type: 'like', - seen: false - }, - { - action: { id: '2' }, - type: 'mention', - seen: true - } - ] - } - }, - config: { + notifications: { + data: [ + { + action: { id: '1' }, + type: 'like', + seen: false + }, + { + action: { id: '2' }, + type: 'mention', + seen: true + } + ] + } + }, + getters: { + mergedConfig: { notificationVisibility: { likes: true, repeats: true, diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -0,0 +1,150 @@ +// import { topoSort } from 'src/services/theme_data/theme_data.service.js' +import { + getAllPossibleCombinations +} from 'src/services/theme_data/iss_utils.js' +import { + init +} from 'src/services/theme_data/theme_data_3.service.js' +import { + basePaletteKeys +} from 'src/services/theme_data/theme2_to_theme3.js' + +describe('Theme Data 3', () => { + describe('getAllPossibleCombinations', () => { + it('test simple 3 values case', () => { + const out = getAllPossibleCombinations([1, 2, 3]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + [1], [2], [3], + [1, 2], [1, 3], [2, 3], + [1, 2, 3] + ]) + }) + + it('test simple 4 values case', () => { + const out = getAllPossibleCombinations([1, 2, 3, 4]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + [1], [2], [3], [4], + [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4], + [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4], + [1, 2, 3, 4] + ]) + }) + + it('test massive 5 values case, using strings', () => { + const out = getAllPossibleCombinations(['a', 'b', 'c', 'd', 'e']).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + // 1 + ['a'], ['b'], ['c'], ['d'], ['e'], + // 2 + ['a', 'b'], ['a', 'c'], ['a', 'd'], ['a', 'e'], + ['b', 'c'], ['b', 'd'], ['b', 'e'], + ['c', 'd'], ['c', 'e'], + ['d', 'e'], + // 3 + ['a', 'b', 'c'], ['a', 'b', 'd'], ['a', 'b', 'e'], + ['a', 'c', 'd'], ['a', 'c', 'e'], + ['a', 'd', 'e'], + + ['b', 'c', 'd'], ['b', 'c', 'e'], + ['b', 'd', 'e'], + + ['c', 'd', 'e'], + // 4 + ['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'e'], + ['a', 'b', 'd', 'e'], + + ['a', 'c', 'd', 'e'], + + ['b', 'c', 'd', 'e'], + // 5 + ['a', 'b', 'c', 'd', 'e'] + ]) + }) + }) + + describe('init', function () { + this.timeout(5000) + + it('Test initialization without anything', () => { + const out = init({ inputRuleset: [], ultimateBackgroundColor: '#DEADAF' }) + + expect(out).to.have.property('eager') + expect(out).to.have.property('lazy') + expect(out).to.have.property('staticVars') + + expect(out.lazy).to.be.an('array') + expect(out.lazy).to.have.lengthOf.above(1) + expect(out.eager).to.be.an('array') + expect(out.eager).to.have.lengthOf.above(1) + expect(out.staticVars).to.be.an('object') + + // check backwards compat/generic stuff + basePaletteKeys.forEach(key => { + expect(out.staticVars).to.have.property(key) + }) + }) + + it('Test initialization with a basic palette', () => { + const out = init({ + inputRuleset: [{ + component: 'Root', + directives: { + '--bg': 'color | #008080', + '--fg': 'color | #00C0A0' + } + }], + ultimateBackgroundColor: '#DEADAF' + }) + + expect(out.staticVars).to.have.property('bg').equal('#008080') + expect(out.staticVars).to.have.property('fg').equal('#00C0A0') + + const panelRule = out.eager.filter(x => { + if (x.component !== 'Panel') return false + return true + })[0] + + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked', { r: 0, g: 128, b: 128 }) + }) + + it('Test initialization with opacity', () => { + const out = init({ + inputRuleset: [{ + component: 'Root', + directives: { + '--bg': 'color | #008080' + } + }, { + component: 'Panel', + directives: { + opacity: 0.5 + } + }], + ultimateBackgroundColor: '#DEADAF' + }) + + expect(out.staticVars).to.have.property('bg').equal('#008080') + + const panelRule = out.eager.filter(x => { + if (x.component !== 'Panel') return false + return true + })[0] + + expect(panelRule).to.have.nested.deep.property('dynamicVars.background', { r: 0, g: 128, b: 128, a: 0.5 }) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked') + // Somewhat incorrect since we don't do gamma correction + // real expectancy should be this: + /* + + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.r').that.is.closeTo(147.0, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.g').that.is.closeTo(143.2, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.b').that.is.closeTo(144.0, 0.01) + + */ + + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.r').that.is.closeTo(88.8, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.g').that.is.closeTo(133.2, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.b').that.is.closeTo(134, 0.01) + }) + }) +}) diff --git a/tools/check-changelog b/tools/check-changelog @@ -6,7 +6,7 @@ git remote add upstream https://git.pleroma.social/pleroma/pleroma-fe.git git fetch upstream ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}:refs/remotes/upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME git diff --raw --no-renames upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME HEAD -- changelog.d | \ - grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security\)$' + grep ' A\t' | grep '\.\(skip\|add\|remove\|change\|fix\|security\)$' ret=$? if [ $ret -eq 0 ]; then diff --git a/yarn.lock b/yarn.lock @@ -24,13 +24,6 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" - integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== - dependencies: - "@babel/highlight" "^7.16.7" - "@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -45,6 +38,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" @@ -116,15 +117,6 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.0" -"@babel/generator@^7.17.3": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad" - integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== - dependencies: - "@babel/types" "^7.17.0" - jsesc "^2.5.1" - source-map "^0.5.0" - "@babel/generator@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.10.tgz#794f328bfabdcbaf0ebf9bf91b5b57b61fa77a2a" @@ -181,6 +173,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" @@ -298,13 +300,6 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" - integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== - dependencies: - "@babel/types" "^7.16.7" - "@babel/helper-environment-visitor@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7" @@ -320,6 +315,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -327,15 +327,6 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" - integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== - dependencies: - "@babel/helper-get-function-arity" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/types" "^7.16.7" - "@babel/helper-function-name@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz#8334fecb0afba66e6d87a7e8c6bb7fed79926b83" @@ -368,19 +359,13 @@ "@babel/template" "^7.20.7" "@babel/types" "^7.21.0" -"@babel/helper-get-function-arity@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" - integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-hoist-variables@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" - integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/types" "^7.16.7" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" @@ -389,6 +374,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.6.tgz#44802d7d602c285e1692db0bad9396d007be2afc" @@ -410,13 +402,6 @@ dependencies: "@babel/types" "^7.21.0" -"@babel/helper-module-imports@^7.0.0": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz#e5a92529f8888bf319a6376abfbd1cebc491ad91" - integrity sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ== - dependencies: - "@babel/types" "^7.7.4" - "@babel/helper-module-imports@^7.0.0-beta.49": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" @@ -437,6 +422,13 @@ dependencies: "@babel/types" "^7.21.4" +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + "@babel/helper-module-transforms@^7.18.6": version "7.18.8" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.8.tgz#4f8408afead0188cfa48672f9d0e5787b61778c8" @@ -535,6 +527,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" @@ -596,13 +593,6 @@ dependencies: "@babel/types" "^7.20.0" -"@babel/helper-split-export-declaration@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" - integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== - dependencies: - "@babel/types" "^7.16.7" - "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" @@ -610,6 +600,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" @@ -625,6 +622,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" @@ -640,6 +642,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" @@ -686,15 +693,6 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/highlight@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" - integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -704,6 +702,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.14.7": version "7.18.11" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" @@ -714,11 +721,6 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== -"@babel/parser@^7.16.7", "@babel/parser@^7.17.3": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.7.tgz#fc19b645a5456c8d6fdb6cecd3c66c0173902800" - integrity sha512-bm3AQf45vR4gKggRfvJdYJ0gFLoCbsPxiFLSH6hTVYABptNHY6l9NrhnucVjQ/X+SPtLANT9lc0fFhikj+VBRA== - "@babel/parser@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.10.tgz#94b5f8522356e69e8277276adf67ed280c90ecc1" @@ -749,6 +751,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" + integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -958,12 +965,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.0.0": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" - integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== +"@babel/plugin-syntax-jsx@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" + integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" @@ -1422,15 +1429,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.0.0", "@babel/template@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" - integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/parser" "^7.16.7" - "@babel/types" "^7.16.7" - "@babel/template@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" @@ -1458,21 +1456,14 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.0.0": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" - integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.3" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.3" - "@babel/types" "^7.17.0" - debug "^4.1.0" - globals "^11.1.0" +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" "@babel/traverse@^7.18.10": version "7.18.10" @@ -1570,6 +1561,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.23.7": + version "7.23.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" + integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.6" + "@babel/types" "^7.23.6" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e" @@ -1578,7 +1585,7 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.4.4": +"@babel/types@^7.16.7", "@babel/types@^7.4.4": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== @@ -1656,13 +1663,13 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@babel/types@^7.7.4": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" - integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA== +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" + integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== dependencies: - esutils "^2.0.2" - lodash "^4.17.13" + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" "@chenfengyuan/vue-qrcode@2.0.0": @@ -1892,6 +1899,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" @@ -2002,10 +2014,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@ruffle-rs/ruffle@0.1.0-nightly.2022.7.12": - version "0.1.0-nightly.2022.7.12" - resolved "https://registry.yarnpkg.com/@ruffle-rs/ruffle/-/ruffle-0.1.0-nightly.2022.7.12.tgz#c2d77fce7a0e98d51a6535371550e0bff019d0ea" - integrity sha512-DFsiT4kdUuSHsYXzHV97e9Ui3FkcsHEg1GyHJipt/lCpCoZ2uRtP41uEz9eNc9ug8jWd7UyXxJmdkkRvs9UHgQ== +"@ruffle-rs/ruffle@0.1.0-nightly.2024.3.17": + version "0.1.0-nightly.2024.3.17" + resolved "https://registry.yarnpkg.com/@ruffle-rs/ruffle/-/ruffle-0.1.0-nightly.2024.3.17.tgz#df3626f7277ed85742a602508c191d3186b0cabc" + integrity sha512-Wl/7CDZSmomOcfBeOYOO6xUUrN7upnGRDJEm3fpCtN3j5kU8dGF8xzaziAttjkD8byLYS09InE7PlUTyyAwCiQ== "@sinclair/typebox@^0.24.1": version "0.24.51" @@ -2198,10 +2210,10 @@ dependencies: "@types/node" "*" -"@ungap/event-target@0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.2.3.tgz#be682c681126dca2371c4e1a1721f8e8bb400905" - integrity sha512-7Bz0qdvxNGV9n0f+xcMKU7wsEfK6PNzo8IdAcOiBgMNyCuU0Mk9dv0Hbd/Kgr+MFFfn4xLHFbuOt820egT5qEA== +"@ungap/event-target@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.2.4.tgz#8b083a62ee665228bac08013fa516a3488528bb8" + integrity sha512-u9Fd3k2qfMtn+0dxbCn/y0pzQ9Ucw6lWR984CrHcbxc+WzcMkJE4VjWHWSb9At40MjwMyHCkJNXroS55Osshhw== "@ungap/promise-all-settled@1.1.2": version "1.1.2" @@ -2213,26 +2225,39 @@ resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz#8d53a1e21347db8edbe54d339902583176de09f2" integrity sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA== -"@vue/babel-helper-vue-transform-on@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc" - integrity sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA== +"@vue/babel-helper-vue-transform-on@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.1.tgz#3a48da809025b9a0eb4f4b3030e0d316c40fac0a" + integrity sha512-jtEXim+pfyHWwvheYwUwSXm43KwQo8nhOBDyjrUITV6X2tB7lJm6n/+4sqR8137UVZZul5hBzWHdZ2uStYpyRQ== -"@vue/babel-plugin-jsx@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz#0c5bac27880d23f89894cd036a37b55ef61ddfc1" - integrity sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.0.0" - "@babel/template" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" - "@vue/babel-helper-vue-transform-on" "^1.0.2" - camelcase "^6.0.0" - html-tags "^3.1.0" +"@vue/babel-plugin-jsx@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.1.tgz#786c5395605a1d2463d6b10d8a7f3abdc01d25ce" + integrity sha512-Yy9qGktktXhB39QE99So/BO2Uwm/ZG+gpL9vMg51ijRRbINvgbuhyJEi4WYmGRMx/MSTfK0xjgZ3/MyY+iLCEg== + dependencies: + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.23.3" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.7" + "@babel/types" "^7.23.6" + "@vue/babel-helper-vue-transform-on" "1.2.1" + "@vue/babel-plugin-resolve-type" "1.2.1" + camelcase "^6.3.0" + html-tags "^3.3.1" svg-tags "^1.0.0" +"@vue/babel-plugin-resolve-type@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.1.tgz#874fb3e02d033b3dd2e0fc883a3d1ceef0bdf39b" + integrity sha512-IOtnI7pHunUzHS/y+EG/yPABIAp0VN8QhQ0UCS09jeMVxgAnI9qdOzO85RXdQGxq+aWCdv8/+k3W0aYO6j/8fQ== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/parser" "^7.23.6" + "@vue/compiler-sfc" "^3.4.15" + "@vue/compiler-core@3.2.45": version "3.2.45" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz#d9311207d96f6ebd5f4660be129fb99f01ddb41b" @@ -2243,6 +2268,17 @@ estree-walker "^2.0.2" source-map "^0.6.1" +"@vue/compiler-core@3.4.15": + version "3.4.15" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.15.tgz#be20d1bbe19626052500b48969302cb6f396d36e" + integrity sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw== + dependencies: + "@babel/parser" "^7.23.6" + "@vue/shared" "3.4.15" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.0.2" + "@vue/compiler-dom@3.2.45": version "3.2.45" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz#c43cc15e50da62ecc16a42f2622d25dc5fd97dce" @@ -2251,6 +2287,14 @@ "@vue/compiler-core" "3.2.45" "@vue/shared" "3.2.45" +"@vue/compiler-dom@3.4.15": + version "3.4.15" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz#753f5ed55f78d33dff04701fad4d76ff0cf81ee5" + integrity sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ== + dependencies: + "@vue/compiler-core" "3.4.15" + "@vue/shared" "3.4.15" + "@vue/compiler-sfc@3.2.45": version "3.2.45" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz#7f7989cc04ec9e7c55acd406827a2c4e96872c70" @@ -2267,6 +2311,21 @@ postcss "^8.1.10" source-map "^0.6.1" +"@vue/compiler-sfc@^3.4.15": + version "3.4.15" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz#4e5811e681955fcec886cebbec483f6ae463a64b" + integrity sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA== + dependencies: + "@babel/parser" "^7.23.6" + "@vue/compiler-core" "3.4.15" + "@vue/compiler-dom" "3.4.15" + "@vue/compiler-ssr" "3.4.15" + "@vue/shared" "3.4.15" + estree-walker "^2.0.2" + magic-string "^0.30.5" + postcss "^8.4.33" + source-map-js "^1.0.2" + "@vue/compiler-ssr@3.2.45": version "3.2.45" resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz#bd20604b6e64ea15344d5b6278c4141191c983b2" @@ -2275,6 +2334,14 @@ "@vue/compiler-dom" "3.2.45" "@vue/shared" "3.2.45" +"@vue/compiler-ssr@3.4.15": + version "3.4.15" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz#a910a5b89ba4f0a776e40b63d69bdae2f50616cf" + integrity sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw== + dependencies: + "@vue/compiler-dom" "3.4.15" + "@vue/shared" "3.4.15" + "@vue/devtools-api@^6.0.0-beta.11": version "6.1.3" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.1.3.tgz#a44c52e8fa6d22f84db3abdcdd0be5135b7dd7cf" @@ -2338,6 +2405,11 @@ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.45.tgz#a3fffa7489eafff38d984e23d0236e230c818bc2" integrity sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg== +"@vue/shared@3.4.15": + version "3.4.15" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.15.tgz#e7d2ea050c667480cb5e1a6df2ac13bcd03a8f30" + integrity sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g== + "@vue/test-utils@2.2.8": version "2.2.8" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.2.8.tgz#2002a2b2c90309f66c5c175b735621438832a610" @@ -2345,17 +2417,17 @@ dependencies: js-beautify "1.14.6" -"@vuelidate/core@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.2.tgz#e874afc830ccc5295e83a0c0a0f0621e084348c9" - integrity sha512-aG1OZWv6xVws3ljyKy/pyxq1rdZZ2ryj+FEREcC9d4GP4qOvNHHZUl/NQxa0Bck3Ooc0RfXU8vwCA9piRoWy6w== +"@vuelidate/core@2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.3.tgz#40468c5ed15b72bde880a026b0699c2f0f1ecede" + integrity sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA== dependencies: vue-demi "^0.13.11" -"@vuelidate/validators@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@vuelidate/validators/-/validators-2.0.0.tgz#1ddd86c6c81b2cfbb5720961e951cc53ec0a80be" - integrity sha512-fQQcmDWfz7pyH5/JPi0Ng2GEgNK1pUHn/Z/j5rG/Q+HwhgIXvJblTPcZwKOj1ABL7V4UVuGKECvZCDHNGOwdrg== +"@vuelidate/validators@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@vuelidate/validators/-/validators-2.0.4.tgz#0a88a7b2b18f15fd9c384095593f369a6f7384e9" + integrity sha512-odTxtUZ2JpwwiQ10t0QWYJkkYrfd0SyFYhdHH44QQ1jDatlZgTh/KRzrWVmn/ib9Gq7H4hFD4e8ahoo5YlUlDw== dependencies: vue-demi "^0.13.11" @@ -2745,14 +2817,14 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -autoprefixer@10.4.14: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== +autoprefixer@10.4.19: + version "10.4.19" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" + integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" + browserslist "^4.23.0" + caniuse-lite "^1.0.30001599" + fraction.js "^4.3.7" normalize-range "^0.1.2" picocolors "^1.0.0" postcss-value-parser "^4.2.0" @@ -2776,12 +2848,12 @@ axios@^1.1.3: form-data "^4.0.0" proxy-from-env "^1.1.0" -babel-loader@9.1.2: - version "9.1.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" - integrity sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA== +babel-loader@9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" + integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== dependencies: - find-cache-dir "^3.3.2" + find-cache-dir "^4.0.0" schema-utils "^4.0.0" babel-plugin-lodash@3.3.4: @@ -2975,15 +3047,15 @@ browserslist@^4.21.3, browserslist@^4.21.4: node-releases "^2.0.6" update-browserslist-db "^1.0.9" -browserslist@^4.21.5: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== +browserslist@^4.23.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" buffer-crc32@~0.2.3: version "0.2.13" @@ -3057,7 +3129,7 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -3087,10 +3159,15 @@ caniuse-lite@^1.0.30001400: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6" integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== -caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001474" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz#13b6fe301a831fe666cce8ca4ef89352334133d5" - integrity sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q== +caniuse-lite@^1.0.30001587: + version "1.0.30001591" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz#16745e50263edc9f395895a7cd468b9f3767cf33" + integrity sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ== + +caniuse-lite@^1.0.30001599: + version "1.0.30001599" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz#571cf4f3f1506df9bf41fcbb6d10d5d017817bce" + integrity sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA== chai-nightwatch@0.5.3: version "0.5.3" @@ -3122,7 +3199,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" dependencies: @@ -3333,6 +3410,11 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3483,19 +3565,19 @@ css-functions-list@^3.1.0: resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b" integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w== -css-loader@6.7.3: - version "6.7.3" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" - integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== +css-loader@6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.10.0.tgz#7c172b270ec7b833951b52c348861206b184a4b7" + integrity sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw== dependencies: icss-utils "^5.1.0" - postcss "^8.4.19" + postcss "^8.4.33" postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" + postcss-modules-local-by-default "^4.0.4" + postcss-modules-scope "^3.1.1" postcss-modules-values "^4.0.0" postcss-value-parser "^4.2.0" - semver "^7.3.8" + semver "^7.5.4" css-minimizer-webpack-plugin@4.2.2: version "4.2.2" @@ -3670,7 +3752,7 @@ debug@4.3.3: dependencies: ms "2.1.2" -debug@4.3.4, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4.3.4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3958,10 +4040,10 @@ electron-to-chromium@^1.4.251: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz#17837b19dafcc43aba885c4689358b298c19b520" integrity sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ== -electron-to-chromium@^1.4.284: - version "1.4.353" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.353.tgz#20e9cb4c83a08e35b3314d3fa8988764c105e6b7" - integrity sha512-IdJVpMHJoBT/nn0GQ02wPfbhogDVpd1ud95lP//FTf5l35wzxKJwibB4HBdY7Q+xKPA1nkZ0UDLOMyRj5U5IAQ== +electron-to-chromium@^1.4.668: + version "1.4.690" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz#dd5145d45c49c08a9a6f7454127e660bdf9a3fa7" + integrity sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA== emoji-regex@^8.0.0: version "8.0.0" @@ -4037,6 +4119,11 @@ entities@^4.2.0, entities@^4.3.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + envinfo@7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -4620,14 +4707,13 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== +find-cache-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" + integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" + common-path-prefix "^3.0.0" + pkg-dir "^7.0.0" find-up@5.0.0, find-up@^5.0.0: version "5.0.0" @@ -4643,7 +4729,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.0.0, find-up@^4.1.0: +find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -4651,6 +4737,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" + integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== + dependencies: + locate-path "^7.1.0" + path-exists "^5.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -4710,10 +4804,10 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== fresh@0.5.2: version "0.5.2" @@ -5095,16 +5189,16 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" -html-tags@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" - integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== - html-tags@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== +html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== + html-webpack-plugin@5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz#826838e31b427f5f7f30971f8d8fa2422dfa6763" @@ -5707,10 +5801,10 @@ js-beautify@1.14.6: glob "^8.0.3" nopt "^6.0.0" -js-cookie@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" - integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== +js-cookie@3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== js-sdsl@^4.1.4: version "4.1.5" @@ -6067,6 +6161,13 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +locate-path@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" + integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + dependencies: + p-locate "^6.0.0" + lodash._arraycopy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" @@ -6302,11 +6403,6 @@ lodash@^4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" -lodash@^4.17.13: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - log-symbols@4.1.0, log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" @@ -6381,6 +6477,13 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" +magic-string@^0.30.5: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -6388,7 +6491,7 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0, make-dir@^3.0.2: +make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -6704,6 +6807,11 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6729,10 +6837,10 @@ nightwatch-axe-verbose@^2.1.0: dependencies: axe-core "^4.6.1" -nightwatch@2.6.20: - version "2.6.20" - resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-2.6.20.tgz#8c3b808f4f33699bcd67987b22e6ebeee61ddc9c" - integrity sha512-XEyxuSGhESdHj4LHqA5snrc/nMgH4tsB/mWrbyGt3EwW1AgjyE7DRzJUbhG7J00Np3Dv3k2nmyJs0Xq0FX/yvQ== +nightwatch@2.6.25: + version "2.6.25" + resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-2.6.25.tgz#99c2abdd8a7f1ce8be2882ac5458a776caf1b5ab" + integrity sha512-aYc5eA6M/iADdbKbD6dMHlhUsaJm/Y4/VOtSHSC23nimGTXMUKbe1Bb14Iz3/SNyz2joHOkpxaDIPIAZCSlOiQ== dependencies: "@nightwatch/chai" "5.0.2" "@nightwatch/html-reporter-template" "0.2.1" @@ -6786,16 +6894,16 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + node-releases@^2.0.5, node-releases@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== - nopt@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" @@ -7012,6 +7120,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -7032,6 +7147,13 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-locate@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" + integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== + dependencies: + p-limit "^4.0.0" + p-try@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" @@ -7099,6 +7221,11 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -7181,12 +7308,12 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== +pkg-dir@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" + integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== dependencies: - find-up "^4.0.0" + find-up "^6.3.0" pngjs@^5.0.0: version "5.0.0" @@ -7323,19 +7450,19 @@ postcss-modules-extract-imports@^3.0.0: resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== +postcss-modules-local-by-default@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz#7cbed92abd312b94aaea85b68226d3dec39a14e6" + integrity sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== +postcss-modules-scope@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz#32cfab55e84887c079a19bbb215e721d683ef134" + integrity sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA== dependencies: postcss-selector-parser "^6.0.4" @@ -7542,6 +7669,15 @@ postcss@^8.4.19: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.33: + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -8095,6 +8231,13 @@ semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6: dependencies: lru-cache "^6.0.0" +semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -8289,10 +8432,6 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -8898,10 +9037,10 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -update-browserslist-db@^1.0.10, update-browserslist-db@^1.0.9: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -8922,6 +9061,14 @@ update-browserslist-db@^1.0.5: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -9459,3 +9606,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==