Compare commits

...

24 Commits

Author SHA1 Message Date
syuilo
629b765abc 12.51.0 2020-10-27 18:29:15 +09:00
syuilo
63a89fa84a カスタム絵文字がつぶれる問題を修正 2020-10-27 18:28:37 +09:00
syuilo
a3f89236a0 New Crowdin updates (#6760)
* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Japanese, Kansai)
2020-10-27 18:24:34 +09:00
syuilo
01560abafb Fix #6762 2020-10-27 18:21:52 +09:00
MeiMei
b5698026ba Fix emojilist.json (#6764) 2020-10-27 18:15:35 +09:00
syuilo
6258ce75b7 Reversi (#6765)
* wip

* wip

* wip

* wip

* Update game.setting.vue

* wip

* wip

* Update game.setting.vue

* wip

* Update game.board.vue

* wip

* Update sidebar.ts
2020-10-27 18:11:41 +09:00
syuilo
6f34c74027 Update popup animation 2020-10-27 16:46:13 +09:00
syuilo
8add4f359b 🎨 2020-10-27 16:45:24 +09:00
syuilo
d8933c135f リモートインスタンス情報を強制更新するAPIを追加 2020-10-27 16:45:14 +09:00
syuilo
eb350e8d6c Better favicon detection 2020-10-27 16:44:54 +09:00
syuilo
615fedd64d Instance Ticker 2020-10-27 16:16:59 +09:00
syuilo
25bd82ecaa Default behavior option for MkA component 2020-10-27 13:53:47 +09:00
syuilo
e0938e5e3a Add animation of context menu 2020-10-25 23:22:27 +09:00
syuilo
ec5e6c8443 APIコンソール 2020-10-25 16:11:08 +09:00
syuilo
25d8077474 Fix bug 2020-10-25 15:45:47 +09:00
syuilo
06083f40d9 🎨 2020-10-25 12:47:40 +09:00
syuilo
ec203f7f79 Use MFM instead of v-html to avoid XSS 2020-10-25 12:25:13 +09:00
syuilo
1b30d7d47a Clean up 2020-10-25 11:29:10 +09:00
syuilo
d9be9c958f 投稿失敗したときにエラー表示するように 2020-10-25 11:19:20 +09:00
syuilo
ed09796e0d 🎨 2020-10-25 11:06:55 +09:00
syuilo
4bfa29c0ab コンテキストメニューの位置計算を改善 2020-10-25 11:01:03 +09:00
syuilo
4804bbb211 インポート/エクスポート設定を復活 2020-10-25 10:48:33 +09:00
syuilo
749102f9c2 ヘッダーにもコンテキストメニュー追加 2020-10-25 09:26:19 +09:00
syuilo
0bcb1434b0 Refactor 2020-10-25 09:15:20 +09:00
68 changed files with 2529 additions and 312 deletions

View File

@@ -1,5 +1,6 @@
---
_lang_: "العربية"
introMisskey: "اهلا بك! ميسكي هو منصة تدوين مصغر لا مركزية ومفتوحة المصدر.\nيمكنك مشاركة \"ملاحظات\" عن ما يجري حولك، وإخبار الجميع عن نفسك 📡\nتسمح لك \"الانفعالات\" بتعبير عن شعورك حول ملاحظات الآخرين 👍\nاكتشف عالمًا جديدًا 🚀"
monthAndDay: "{day}/{month}"
search: "البحث"
notifications: "الإشعارات"
@@ -32,6 +33,7 @@ favorite: "إضافة إلى المفضلة"
favorites: "المفضلات"
unfavorite: "إزالة من المفضلة"
pin: "دبّسها على الصفحة الشخصية"
unpin: "ألغ تثبيتها من ملفك الشخصي"
copyContent: "انسخ المحتوى"
copyLink: "انسخ الرابط"
delete: "حذف"
@@ -78,6 +80,7 @@ followRequest: "طلب اشتراك"
followRequests: "طلبات الإشتراك"
unfollow: "إلغاء الاشتراك"
followRequestPending: "طلبات الإشتراك المعلّقة"
enterEmoji: "أدخل إيموجي"
unrenote: "إلغاء مشاركة الملاحظة"
quote: "اقتبس"
pinnedNote: "ملاحظة مدبسة"
@@ -93,6 +96,8 @@ mute: "اكتم"
unmute: "إلغاء الكتم"
block: "احجب"
unblock: "إلغاء الحجب"
suspend: "علِق"
unsuspend: "ألغ التعليق"
blockConfirm: "أمتأكد من حجب هذا الحساب؟"
unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟"
selectList: "اختر قائمة"
@@ -304,6 +309,7 @@ noteOf: "ملاحظات {user}"
inviteToGroup: "دعوة إلى فريق"
noMessagesYet: "ليس هناك رسائل بعد"
newMessageExists: "لقد تلقيت رسالة جديدة"
invitations: "دعوة"
invitationCode: "رمز الدعوة"
checking: "التحقق جارٍ"
available: "متوفر"
@@ -374,6 +380,8 @@ smtpHost: "المضيف"
smtpUser: "اسم المستخدم"
smtpPass: "الكلمة السرية"
display: "المظهر"
_reversi:
total: "المجموع"
_channel:
featured: "المتداوَلة"
_sidebar:

View File

@@ -18,7 +18,7 @@ instance: "Instanz"
settings: "Einstellungen"
basicSettings: "Allgemeine Einstellungen"
otherSettings: "Andere Einstellungen"
openInWindow: "In neuem Fenster öffnen"
openInWindow: "In Fenster öffnen"
profile: "Profil"
timeline: "Chronik"
noAccountDescription: "Dieser Nutzer hat seine Profilbeschreibung noch nicht ausgefüllt."
@@ -412,6 +412,7 @@ noMessagesYet: "Noch keine Nachrichten"
newMessageExists: "Du hast eine neue Nachricht"
onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden"
signinRequired: "Anmeldung erforderlich"
invitations: "Einladen"
invitationCode: "Einladungscode"
checking: "Wird überprüft..."
available: "Verfügbar"
@@ -484,7 +485,7 @@ newNoteRecived: "Es gibt neue Notizen"
sounds: "Töne"
listen: "Anhören"
none: "Keine"
showInPage: "In Seiten anzeigen"
showInPage: "In Seite anzeigen"
popout: "Pop-Up"
volume: "Lautstärke"
details: "Details"
@@ -593,6 +594,18 @@ fillAbuseReportDescription: "Bitte gib Details für diese Meldung an. Falls es s
abuseReported: "Die Meldung wurde versendet. Vielen Dank."
send: "Senden"
abuseMarkAsResolved: "Meldung als gelöst markieren"
openInNewTab: "In neuem Tab öffnen"
openInSideView: "In Seitenansicht öffnen"
defaultNavigationBehaviour: "Standardnavigationsverhalten"
editTheseSettingsMayBreakAccount: "Bei Bearbeitung dieser Einstellungen besteht die Gefahr, dein Benutzerkonto zu beschädigen."
instanceTicker: "Instanz-Informationen der Notiz"
random: "Zufällig"
_reversi:
total: "Gesamt"
_instanceTicker:
none: "Nie anzeigen"
remote: "Für Benutzer fremder Instanzen anzeigen"
always: "Immer anzeigen"
_serverDisconnectedBehavior:
reload: "Automatisch aktualisieren"
dialog: "Warnungsfenster zeigen"

View File

@@ -18,7 +18,7 @@ instance: "Instance"
settings: "Settings"
basicSettings: "Basic Settings"
otherSettings: "Other Settings"
openInWindow: "Open in new window"
openInWindow: "Open in window"
profile: "Profile"
timeline: "Timeline"
noAccountDescription: "This user has not written their bio yet."
@@ -412,6 +412,7 @@ noMessagesYet: "No messages yet"
newMessageExists: "You've got a new message"
onlyOneFileCanBeAttached: "You can only attach one file to a message"
signinRequired: "Please sign in"
invitations: "Invite"
invitationCode: "Invitation code"
checking: "Checking"
available: "Available"
@@ -484,7 +485,7 @@ newNoteRecived: "You've got a new note"
sounds: "Sounds"
listen: "Listen"
none: "None"
showInPage: "Show in Pages"
showInPage: "Show in page"
popout: "Pop-out"
volume: "Volume"
details: "Details"
@@ -593,6 +594,18 @@ fillAbuseReportDescription: "Please fill in the report details. If it is about a
abuseReported: "Your report has been sent. Thank you very much."
send: "Send"
abuseMarkAsResolved: "Mark report as resolved"
openInNewTab: "Open in new tab"
openInSideView: "Open in side view"
defaultNavigationBehaviour: "Default navigation behavior"
editTheseSettingsMayBreakAccount: "Editing these settings may damage your account."
instanceTicker: "Instance information of the Note"
random: "Random"
_reversi:
total: "Total"
_instanceTicker:
none: "Never show"
remote: "Show for remote users"
always: "Always show"
_serverDisconnectedBehavior:
reload: "Automatically reload"
dialog: "Show warning dialog"

View File

@@ -16,6 +16,9 @@ noNotes: "No hay notas"
noNotifications: "No hay notificaciones"
instance: "Instancia"
settings: "Configuración"
basicSettings: "Configuración Básica"
otherSettings: "Configuración avanzada"
openInWindow: "Abrir en una ventana"
profile: "Perfil"
timeline: "Linea de tiempo"
noAccountDescription: "Este usuario no tiene una descripción"
@@ -40,6 +43,7 @@ deleteAndEditConfirm: "¿Quieres borrar y editar este nota? Las reacciones, reno
addToList: "Agregar a lista"
sendMessage: "Énviar mensaje"
copyUsername: "Copiar nombre de usuario"
searchUser: "Búsqueda de usuarios"
reply: "Responder"
loadMore: "Ver más"
youGotNewFollower: "te ha seguido"
@@ -66,7 +70,11 @@ followers: "Seguidores"
followsYou: "Te sigue"
createList: "Crear lista"
manageLists: "Administrar listas"
error: "Error"
somethingHappened: "Ocurrió un error"
retry: "Reintentar"
pageLoadError: "Error al leer la página"
pageLoadErrorDescription: "Normalmente es debido a la red o al caché del navegador. Por favor limpie el caché o intente más tarde."
enterListName: "Ingrese nombre de lista"
privacy: "Privacidad"
makeFollowManuallyApprove: "Aprobar manualmente las solicitudes de seguimiento"
@@ -105,6 +113,8 @@ unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
selectList: "Seleccione una lista"
selectAntenna: "Seleccionar antena"
selectWidget: "Seleccionar widget"
editWidgets: "Editar widgets"
editWidgetsExit: "Terminar edición"
customEmojis: "Emojis personalizados"
emoji: "Emoji"
emojiName: "Nombre del emoji"
@@ -402,6 +412,7 @@ noMessagesYet: "Aún no hay chat"
newMessageExists: "Tienes un mensaje nuevo"
onlyOneFileCanBeAttached: "Solo se puede añadir un archivo al mensaje"
signinRequired: "Iniciar sesión"
invitations: "Invitar"
invitationCode: "Código de invitación"
checking: "Comprobando"
available: "Disponible"
@@ -443,6 +454,7 @@ total: "Total"
weekOverWeekChanges: "Dif semanal"
dayOverDayChanges: "Dif diaria"
appearance: "Apariencia"
clientSettings: "Configuración del cliente"
accountSettings: "Ajustes de cuenta"
promotion: "Promovido"
promote: "Promover"
@@ -473,6 +485,8 @@ newNoteRecived: "Tienes una nota nuevo"
sounds: "Sonidos"
listen: "Escuchar"
none: "Ninguna"
showInPage: "Mostrar en la página"
popout: "Popout"
volume: "Volumen"
details: "Detalles"
chooseEmoji: "Elije un emoji"
@@ -566,6 +580,27 @@ notificationSetting: "Ajustes de Notificaciones"
notificationSettingDesc: "Por favor elija el tipo de notificación a mostrar"
useGlobalSetting: "Usar ajustes globales"
useGlobalSettingDesc: "Al activarse, se usará la configuración de notificaciones de la cuenta, al desactivarse se pueden hacer configuraciones particulares."
other: "Otro"
regenerateLoginToken: "Regenerar token de login"
regenerateLoginTokenDescription: "Regenerar el token usado internamente durante el login. No siempre es necesario hacerlo. Al hacerlo de nuevo, se deslogueará en todos los dispositivos."
setMultipleBySeparatingWithSpace: "Puedes añadir mas de uno, separado por espacios."
fileIdOrUrl: "Id del archivo o URL"
chatOpenBehavior: "Comportamiento al abrir el chat"
sample: "Muestra"
abuseReports: "Reportes"
reportAbuse: "Reportar"
reportAbuseOf: "Reportar a {name}"
fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en particular, ingrese la URL de esta."
abuseReported: "Se ha enviado el reporte. Muchas gracias."
send: "Enviar"
abuseMarkAsResolved: "Marcar reporte como resuelto"
openInNewTab: "Abrir en una Nueva Pestaña"
openInSideView: "Abrir en una vista al costado"
defaultNavigationBehaviour: "Navegación por defecto"
editTheseSettingsMayBreakAccount: "Editar estas configuraciones puede dañar su cuenta."
random: "Aleatorio"
_reversi:
total: "Total"
_serverDisconnectedBehavior:
reload: "Recargar automáticamente"
dialog: "Mostrar diálogo de advertencia"

View File

@@ -411,6 +411,7 @@ noMessagesYet: "Pas encore discuté"
newMessageExists: "Vous avez un nouveau message"
onlyOneFileCanBeAttached: "Vous ne pouvez joindre quun seul fichier au message"
signinRequired: "Veuillez vous connecter"
invitations: "Inviter"
invitationCode: "Code dinvitation"
checking: "Vérification"
available: "Disponible"
@@ -580,6 +581,9 @@ regenerateLoginToken: "Régénérer le jeton de connexion"
setMultipleBySeparatingWithSpace: "Vous pouvez définir plus dun, séparés par des espaces."
fileIdOrUrl: "ID du fichier ou URL"
chatOpenBehavior: "Comportement de la fenêtre de discussion lors de son ouverture"
random: "Aléatoire"
_reversi:
total: "Total"
_serverDisconnectedBehavior:
reload: "Rechargement automatique"
_channel:

View File

@@ -412,6 +412,7 @@ noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "ログインしてください"
invitations: "招待"
invitationCode: "招待コード"
checking: "確認しています"
available: "利用できます"
@@ -597,6 +598,48 @@ openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション"
editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。"
instanceTicker: "ノートのインスタンス情報"
waitingFor: "{x}を待っています"
random: "ランダム"
_reversi:
reversi: "リバーシ"
gameSettings: "対局の設定"
chooseBoard: "ボードを選択"
blackOrWhite: "先行/後攻"
blackIs: "{name}が黒(先行)"
rules: "ルール"
botSettings: "Botのオプション"
thisGameIsStartedSoon: "対局は数秒後に開始されます"
waitingForOther: "相手の準備が完了するのを待っています"
waitingForMe: "あなたの準備が完了するのを待っています"
waitingBoth: "準備してください"
ready: "準備完了"
cancelReady: "準備を再開"
opponentTurn: "相手のターンです"
myTurn: "あなたのターンです"
turnOf: "{name}のターンです"
pastTurnOf: "{name}のターン"
surrender: "投了"
surrendered: "投了により"
drawn: "引き分け"
won: "{name}の勝ち"
black: "黒"
white: "白"
total: "合計"
turnCount: "{count}ターン目"
myGames: "自分の対局"
allGames: "みんなの対局"
ended: "終了"
playing: "対局中"
isLlotheo: "石の少ない方が勝ち(ロセオ)"
loopedMap: "ループマップ"
canPutEverywhere: "どこでも置けるモード"
_instanceTicker:
none: "表示しない"
remote: "リモートユーザーに表示"
always: "常に表示"
_serverDisconnectedBehavior:
reload: "自動でリロード"

View File

@@ -350,6 +350,7 @@ notFoundDescription: "指定されたURLに該当するページはあらへん
close: "さいなら"
joinedGroups: "参加しとるグループ"
invites: "来てや"
invitations: "来てや"
smtpHost: "ホスト"
smtpUser: "ユーザー名"
smtpPass: "パスワード"

View File

@@ -401,6 +401,7 @@ noMessagesYet: "아직 대화가 없습니다"
newMessageExists: "새 메시지가 있습니다"
onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
signinRequired: "로그인 해주세요"
invitations: "초대"
invitationCode: "초대 코드"
checking: "확인하는 중입니다"
available: "사용 가능합니다"
@@ -548,6 +549,9 @@ copy: "복사"
logs: "로그"
database: "데이터베이스"
channel: "채널"
random: "랜덤"
_reversi:
total: "합계"
_channel:
create: "채널 생성"
setBanner: "배너 설정"

View File

@@ -18,7 +18,7 @@ instance: "Инстанс"
settings: "Настройки"
basicSettings: "Основные настройки"
otherSettings: "Прочие настройки"
openInWindow: "Открыть в окне"
openInWindow: "Открывать в плавающих окнах"
profile: "Профиль"
timeline: "Лента"
noAccountDescription: "Пользователь ничего не написал про себя"
@@ -330,21 +330,24 @@ antennaKeywords: "Ключевые слова"
antennaExcludeKeywords: "Исключения"
antennaKeywordsDescription: "Разделяйте пробелами для условия \"И\". Разделяйте переводом строки для \"ИЛИ\"."
notifyAntenna: "Уведомлять о новых записях"
withFileAntenna: "Включать только заметки с вложениями"
serviceworker: "ServiceWorker"
enableServiceworker: "Включить ServiceWorker"
antennaUsersDescription: "Располагать каждое имя с новой строки"
caseSensitive: "С учётом регистра"
withReplies: "Включить ответы"
connectedTo: "Вы подключены к следующим аккаунтам"
notesAndReplies: "Посты и ответы"
withFiles: "с файлами"
silence: "Заглушить"
silenceConfirm: "Вы уверены, что хотите заглушить этого пользователя?"
unsilenceConfirm: "Ты уверен, что хочешь отменить глушение этого пользователя?"
silenceConfirm: " Заглушить этого пользователя? Уверены?"
unsilence: "Снять глушение"
unsilenceConfirm: "Снять глушение с этого пользователя? Уверены?"
popularUsers: "Популярные пользователи"
recentlyUpdatedUsers: "Недавно активные"
recentlyUpdatedUsers: "Активные последнее время"
recentlyRegisteredUsers: "Недавно зарегистрированные пользователи"
recentlyDiscoveredUsers: "Недавно обнаруженные пользователи"
exploreUsersCount: "{count} пользователей"
exploreUsersCount: "Пользователей: {count}"
exploreFediverse: "Исследуйте Fediverse"
popularTags: "Популярные теги"
userList: "Списки"
@@ -409,6 +412,7 @@ noMessagesYet: "Сообщений нет"
newMessageExists: "Новое сообщение"
onlyOneFileCanBeAttached: "К сообщению можно прикрепить только один файл."
signinRequired: "Пожалуйста, войдите."
invitations: "Пригласить"
invitationCode: "Код приглашения"
checking: "Проверка"
available: "Доступен"
@@ -420,9 +424,10 @@ weakPassword: "Слабый пароль"
normalPassword: "Обычный пароль"
strongPassword: "Надёжный пароль"
passwordMatched: "Подходящий пароль"
passwordNotMatched: "Пароль не найден"
passwordNotMatched: "Пароль не совпадает"
signinWith: "Войти в систему с помощью {x}"
signinFailed: "Невозможно войти в систему. Введенное вами имя пользователя или пароль неверны."
tapSecurityKey: "Нажмите на свой электронный ключ"
or: "или"
uiLanguage: "Язык интерфейса"
groupInvited: "Приглашение в группу"
@@ -431,7 +436,7 @@ useOsNativeEmojis: "Используйте родной для ОС Emojis"
youHaveNoGroups: "Группы не найдены"
joinOrCreateGroup: "Получите приглашение присоединиться к группам или вы можете создать свою собственную группу."
noHistory: "Ничего не найдено"
disableAnimatedMfm: "Отключение MFM в анимации"
disableAnimatedMfm: "Отключение анимированной разметки MFM"
doing: "В пути"
category: "Категория"
tags: "Теги"
@@ -455,15 +460,24 @@ promotion: "Опубликовано"
promote: "Продвинуть"
numberOfDays: "Количество дней"
hideThisNote: "Спрятать эту запись"
showFeaturedNotesInTimeline: "Показывать в ленте заметки из подборки сайта"
objectStorage: "Хранилище"
useObjectStorage: "Занято в хранилище"
objectStorageBaseUrl: "Базовый URL-адрес"
objectStorageBaseUrl: "Базовый адрес"
objectStorageBaseUrlDesc: "Это начальная часть адреса, используемого CDN или прокси, например для S3: https://<bucket>.s3.amazonaws.com, или дя GCS: 'https://storage.googleapis.com/<bucket>'"
objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Укажите название контейнера (Bucket) который используется на выбранном сервисе."
objectStoragePrefix: "Префикс"
objectStoragePrefixDesc: "Файлы будут храниться в директории, соответствующей указанному здесь префиксу пути"
objectStorageEndpoint: "Конечная точка"
objectStorageEndpointDesc: "Если используете AWS S3, оставьте пустым. В остальных случаях укажите конечную точку (endpoint) в форме «<host>» или «<host>:<port>», так, как это описано в руководстве той службы, которую собираетесь использовать."
objectStorageRegion: "Регион"
objectStorageRegionDesc: "Укажите регион, например xx-east-1. Если ваша служба не различает регионы, оставьте поле пустым, или впишите us-east-1."
objectStorageUseSSL: "Использовать SSL"
objectStorageUseSSLDesc: "Отключите, если не собираетесь использовать протокол HTTPS для обмена по API."
objectStorageUseProxy: "Использовать прокси"
objectStorageUseProxyDesc: "Отключите, если не будете испоьзовать прокси для соединений по протоколу ObjectStorage."
objectStorageSetPublicRead: "Устанавливать public-read при загрузке на сервер"
serverLogs: "Журнал сервера"
deleteAll: "Удалить всё"
showFixedPostForm: "Показывать поле для ввода новой заметки наверху ленты."
@@ -493,6 +507,7 @@ scratchpadDescription: "Scratchpad предоставляет экспериме
output: "Выходы"
script: "Скрипт"
disablePagesScript: "Отключение скриптов в Pages"
updateRemoteUser: "Обновить данные пользователя с его сервера"
deleteAllFiles: "Удалить все файлы"
deleteAllFilesConfirm: "Вы хотите удалить все файлы?"
removeAllFollowing: "Удалить всех подписчиков"
@@ -505,23 +520,37 @@ addItem: "Добавить элемент"
rooms: "Комнаты"
relays: "Ретрансляторы"
addRelay: "Добавить ретранслятор"
inboxUrl: "URL ящика входящих сообщений"
addedRelays: "Добавленные ретрансляторы"
serviceworkerInfo: "Нужно включить, чтобы работали push-уведомления."
deletedNote: "Удалённая заметка"
invisibleNote: "Личное сообщение"
enableInfiniteScroll: "Включить бесконечную прокрутку"
visibility: "Видимость"
poll: "Опрос"
useCw: "Скрывать содержимое под предупреждением"
enablePlayer: "Включить плеер"
disablePlayer: "Выключить плеер"
expandTweet: "Разавернуть твит"
themeEditor: "Редактор темы"
description: "Описание"
author: "Автор"
leaveConfirm: "Вы не сохранили изменения. Хотите выйти и потерять их?"
manage: "Управление"
plugins: "Плагины"
deck: "Панель"
undeck: окинуть панель"
pluginInstallWarn: "Пожалуста, не устанавливайте плагины, которым не доверяете."
deck: ульт"
undeck: "Покинуть пульт"
useBlurEffectForModal: "Размывка под формой поверх всего"
generateAccessToken: "Создать токен доступа"
permission: "Разрешения"
enableAll: "Включить все"
disableAll: "Выключить всё"
tokenRequested: "Открыть доступ к учётной записи"
pluginTokenRequestedDescription: "Этот плагин сможет пользоваться разрешениями, установленными здесь."
notificationType: "Тип уведомления"
edit: "Изменить"
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
emailConfig: "Настройки почтового сервера"
enableEmail: "Активировать функцию доставки электронной почты"
emailConfigInfo: "Он используется для подтверждения адреса электронной почты и сброса пароля."
@@ -536,6 +565,7 @@ smtpSecure: "Использовать SSL/TLS для SMTP-соединений"
smtpSecureInfo: "Выключите его при использовании STARTTLS."
testEmail: "Проверка Email"
wordMute: "Скрытие слов"
userSaysSomething: "{name} что-то сообщает"
makeActive: "Активировать"
display: "Показать"
copy: "Копировать"
@@ -551,18 +581,26 @@ notificationSettingDesc: "Выберите тип уведомлений для
useGlobalSetting: "Использовать глобальные настройки"
useGlobalSettingDesc: "Использовать глобальные настройки"
other: "Другие"
regenerateLoginToken: "Восстановить токен входа"
regenerateLoginTokenDescription: "Восстановите токен, используемый внутри во время входа. Обычно в этом нет необходимости. При восстановлении все устройства будут отключены."
regenerateLoginToken: "Создать новый токен для входа"
regenerateLoginTokenDescription: "Создаёт новый токен, используемый внутри программы во время входа. Обычно в этом нет необходимости. При создании все устройства будут отключены."
setMultipleBySeparatingWithSpace: "Вы можете установить несколько, разделив их пробелами."
fileIdOrUrl: "ID файла или URL-адрес"
chatOpenBehavior: "Поведение окна чата при открытии"
sample: "Пример"
abuseReports: "Отчеты"
reportAbuse: "Отчеты"
reportAbuseOf: "Пожаловаться на пользователя {name}"
fillAbuseReportDescription: "Пожалуйста, заполните данные отчета. Если речь идет о конкретном сообщении, пожалуйста, укажите его URL."
abuseReported: "Содержимое отправлено. Спасибо за сообщение"
send: "Отправить"
abuseMarkAsResolved: "Отметить отчет как решенный"
openInNewTab: "Открыть в новой вкладке"
openInSideView: "Открывать в боковой колонке"
defaultNavigationBehaviour: "Поведение навигации по умолчанию"
editTheseSettingsMayBreakAccount: "От изменений в этих настройках ваша учётная запись может поломаться."
random: "Случайные"
_reversi:
total: "Всего"
_serverDisconnectedBehavior:
reload: "Автоматическая перезагрузка"
dialog: "Предупреждение"
@@ -585,6 +623,8 @@ _wordMute:
muteWords: "Скрыть слово"
muteWordsDescription: "Разделяйте пробелами для условия \"И\". Разделяйте переводом строки для \"ИЛИ\"."
muteWordsDescription2: "Округляйте ключевые слова слэшами для использования регулярных выражений."
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
soft: "Лёгкий "
hard: "Сложный"
mutedNotes: "Скрытые посты"
@@ -597,11 +637,12 @@ _theme:
alreadyInstalled: "Тема уже установлена"
invalid: "Формат темы некорректный "
make: "Создать тему"
base: "База"
base: "Основа"
addConstant: "Добавить константу"
constant: "Константа"
defaultValue: "Значение по умолчанию"
defaultValue: "Исходное содержимое"
color: "Цвет"
refProp: "Ссылка на свойство"
refConst: "Ссылка на константу"
key: "Ключ"
func: "Функции"
@@ -609,26 +650,63 @@ _theme:
argument: "Аргумент"
basedProp: "Указанное свойство"
alpha: "Непрозрачность"
darken: "Темный"
darken: "Затемнение"
lighten: "Осветление"
inputConstantName: "Введите имя для константы"
importInfo: "Если вы введете код темы здесь, вы можете импортировать его в редактор тем."
deleteConstantConfirm: "Вы действительно хотите удалить константу {const}?"
keys:
bg: "Справочная информация"
accent: "Акцент"
bg: "Фон"
fg: "Текст"
navHoverFg: "Текст на боковой панели (Навести)"
focus: "Фокус"
indicator: "Индикатор"
panel: "Панель"
shadow: "Тень"
header: "Заголовок"
navBg: "Фон боковой панели"
navFg: "Текст на боковой панели"
navHoverFg: "Текст на боковой панели (под указателем)"
navActive: "Текст на боковой панели (активирован)"
navIndicator: "Индикатор на боковой панели"
link: "Ссылка"
hashtag: "Хэштеги"
hashtag: "Хэштег"
mention: "Упоминание"
mentionMe: "Упоминания вас"
renote: "Репост"
divider: "Разделительная полоса"
infoBg: "Справочная информация"
infoFg: "Текст информации"
modalBg: "Фон формы поверх страницы"
divider: "Разделитель"
scrollbarHandle: "Ползунок прокрутки"
scrollbarHandleHover: "Ползунок прокрутки (под указателем)"
dateLabelFg: "Текст отметки даты"
infoBg: "Фон сообщения"
infoFg: "Текст сообщения"
infoWarnBg: "Фон предупреждения"
infoWarnFg: "Текст предупреждения"
cwBg: "Фон предупреждения о содержимом"
cwFg: "Текст предупреждения о содержимом"
cwHoverBg: "Фон предупреждения о содержимом (под указателем)"
toastBg: "Фон оповещения"
toastFg: "Текст оповещения"
buttonBg: "Фон кнопки"
buttonHoverBg: "Текст кнопки"
inputBorder: "Рамка поля ввода"
listItemHoverBg: "Фон пункта списка (под указателем)"
driveFolderBg: "Фон папки «Диска»"
wallpaperOverlay: "Слой обоев"
badge: "Значок"
messageBg: "Фон беседы"
accentDarken: "Фон (затемнённый)"
accentLighten: "Фон (осветлённый)"
fgHighlighted: "Подсвеченный текст"
_sfx:
note: "Заметки"
noteMy: "Собственные заметки"
notification: "Уведомления"
chat: "Сообщения"
chatBg: "Сообщения (фон)"
antenna: "Антенна"
channel: "Канал"
_ago:
unknown: "Когда-то"
future: "Из будущего"
@@ -671,10 +749,11 @@ _tutorial:
_2fa:
alreadyRegistered: "Настройка завершена"
registerDevice: "Зарегистрируйте ваше устройство"
step1: "Во-первых, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения"
step3: "Введите токен, отображаемый в приложении, и все готово"
step4: "Когда вы войдете в систему, вы можете ввести свой токен тем же способом."
registerKey: "Зарегистрировать ключ"
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
step3: "И наконец, введите код, который покажет приложение."
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
_permissions:
"read:account": "Просматривать данные учётной записи"
@@ -706,7 +785,18 @@ _permissions:
"read:channels": "Смотреть каналы"
"write:channels": "Изменять каналы"
_auth:
shareAccess: "Дать доступ для «{name}» к вашей учётной записи?"
shareAccessAsk: "Уверены, что хотите дать приложению доступ к своей учётной записи?"
permissionAsk: "Приложение запрашивает следующие разрешения:"
pleaseGoBack: "Вернитесь, пожалуйста, в приложение"
callback: "Возврат в приложение"
denied: "Доступ закрыт"
_antennaSources:
all: "Все заметки"
homeTimeline: "Заметки тех на которых вы подписаны"
users: "Заметки выбранных пользователей"
userList: "Заметки пользователей из выбранных списков"
userGroup: "Заметки от пользователей из заданной группы"
_weekday:
sunday: "Воскресенье"
monday: "Понедельник"
@@ -731,8 +821,8 @@ _widgets:
_cw:
hide: "Спрятать"
show: "Показать еще"
chars: "{count} символов"
files: "{count} файлов"
chars: "символов: {count}"
files: "файлов: {count}"
_poll:
noOnlyOneChoice: "Нужно хотя бы два варианта."
choiceN: "Выбор {n}"
@@ -799,183 +889,412 @@ _charts:
activeUsers: "Активные пользователи"
notesIncDec: "Изменение числа заметок"
localNotesIncDec: "Изменения числа локальных заметок"
remoteNotesIncDec: "Изменения числа заметок с других сайтов"
notesTotal: "Общее количество заметок"
filesIncDec: "Изменения числа файлов"
filesTotal: "Суммарное количество файлов"
storageUsageIncDec: "Изменения заполнения хранилища"
storageUsageTotal: "Суммарное заполнение хранилища"
_instanceCharts:
requests: "Запросы"
users: "Изменение числа пользователей"
usersTotal: "Суммарное количество пользователей"
notes: "Изменение числа заметок"
notesTotal: "Суммарное количество заметок"
ff: "Изменения числа подписчиков"
ffTotal: "Суммарное количество подписчиков"
cacheSize: "Изменения размера кэша"
cacheSizeTotal: "Суммарный размер кэша"
files: "Изменения числа файлов"
filesTotal: "Суммарное количество файлов"
_timelines:
home: "Главная"
home: "Персональное"
local: "Местное"
social: "Социальное"
global: "Глобальное"
_rooms:
roomOf: "Комната {user}"
addFurniture: "Добавить обстановку"
translate: "Передвинуть"
rotate: "Повернуть"
exit: "Выход"
remove: "Выбросить"
clear: "Очистить"
clearConfirm: "Уверены что стоит убрать всю обстановку из вашей комнаты?"
leaveConfirm: "Измнения не сохранены, правда покинуть комнату?"
chooseImage: "Выберите изображение"
roomType: "Стиль комнаты"
carpetColor: "Цвет ковра"
_roomType:
default: "По умолчанию"
washitsu: "Японская"
_furnitures:
milk: "Пакет молока"
bed: "Кровать"
low-table: "Журнальный стол"
desk: "Письменный стол"
chair: "Стул"
chair2: "Стул 2"
fan: "Вентилятор"
pc: "Системный блок"
plant: "Растение в горшке"
plant2: "Растение в горшке 2"
eraser: "Ластик"
pencil: "Карандаш"
pudding: "Пудинг"
cardboard-box: "Картонная коробка"
cardboard-box2: "Картонная коробка 2"
cardboard-box3: "Картонная коробка 3"
book: "Книга"
book2: "Книга про Misskey"
piano: "Пианино"
facial-tissue: "Салфетки"
server: "Сервер"
moon: "Луна"
corkboard: "Пробковая доска"
mousepad: "Коврик для мыши"
monitor: "Монитор"
keyboard: "Клавиатура"
carpet-stripe: "Полосатый ковёр"
mat: "Мат"
color-box: "Книжная полка"
wall-clock: "Настенные часы"
photoframe: "Картина в раме"
cube: "Куб"
tv: "Телевизор"
pinguin: "Пингвин"
rubik-cube: "Кубик Рубика"
poster-h: "Плакат (альбомная ориентация)"
poster-v: "Плакат (портретная ориентация)"
sofa: "Диван"
spiral: "Спиральная лестница"
bin: "Мусорное ведро"
cup-noodle: "Стакан лапши"
holo-display: "Голографический проектор"
energy-drink: "Банка энергетического напитка"
doll-ai: "Кукла Ай-тян"
banknote: "Пачка денег"
_pages:
newPage: "Создать страницу"
editPage: "Править страницу"
readPage: "Читать страницу"
created: "Страница успешно создана"
updated: "Страница успешно обновлена"
deleted: "Страница успешно удалена"
nameAlreadyExists: "Указанный адрес страницы уже существует"
invalidNameTitle: "Указанный адрес страницы недопустим"
invalidNameText: "Проверьте, что не оставили поле пустым."
editThisPage: "Правка этой страницы"
viewSource: "Просмотр исходника"
viewPage: "Смотреть страницы"
title: "Заголовок."
like: "Нравится"
unlike: "Отменить «нравится»"
my: "Свои страницы"
liked: "Понравившиеся страницы"
inspector: "Инспектор"
content: "Содержимое"
variables: "Переменные"
title: "Заголовок"
url: "URL страницы"
summary: "Краткое содержание"
alignCenter: "Выровнять элементы по центру"
hideTitleWhenPinned: "Скрыть заголовок страницы при привязке к профилю"
font: "Шрифт"
fontSerif: "Антиква (с засечками)"
fontSansSerif: "Гротеск (без засечек)"
eyeCatchingImageSet: "Добавить картинку для привлечения внимания"
eyeCatchingImageRemove: "Убрать картинку для привлечения внимания"
chooseBlock: "Добавить блок"
selectType: "Выберите вид"
enterVariableName: "Ведите имя переменной"
variableNameIsAlreadyUsed: "Это имя уже есть у другой переменной"
contentBlocks: "Содержательные"
inputBlocks: "Для ввода"
specialBlocks: "Особые"
blocks:
text: "Текст"
textarea: "Текст в рамке"
section: "Раздел"
image: "Изображения"
button: "Кнопка"
if: "Если"
if: "Условный"
_if:
variable: "Переменная"
post: "Форма отправки"
post: "Создание заметки"
_post:
text: "Содержимое"
textInput: "Ввод текста"
text: "Текст"
attachCanvasImage: "Прикрепить изображение с холста"
canvasId: "Метка холста"
textInput: "Поле ввода текста"
_textInput:
name: "Имя переменной"
text: "Заголовок."
default: "Значение по умолчанию"
textareaInput: "Многострочный ввод текста"
text: "Подпись"
default: "Исходное содержимое"
textareaInput: "Многострочное поле ввода текста"
_textareaInput:
name: "Имя переменной"
text: "Заголовок."
default: "Значение по умолчанию"
text: "Подпись"
default: "Исходное содержимое"
numberInput: "Поле для ввода числа"
_numberInput:
name: "Имя переменной"
text: "Заголовок."
default: "Значение по умолчанию"
text: "Подпись"
default: "Исходное значение"
canvas: "Холст"
_canvas:
id: "Метка холста"
width: "Ширина"
height: "Высота"
switch: "Выключатель"
_switch:
name: "Имя переменной"
text: "Заголовок."
default: "Значение по умолчанию"
text: "Подпись"
default: "Исходное содержимое"
counter: "Кнопка со счётчиком"
_counter:
name: "Имя переменной"
text: "Заголовок."
text: "Надпись"
inc: "Увеличивать на"
_button:
text: "Заголовок."
text: "Надпись"
colored: "Цветная"
action: "Действие по нажатию"
_action:
dialog: "Показать всплывающий текст"
_dialog:
content: "Содержимое"
content: "Всплывающий текст"
resetRandom: "Сброс генератора случайности"
pushEvent: "Вызвать событие"
_pushEvent:
no-variable: "Не найдено"
callAiScript: "Справка AiScript"
event: "Имя события"
message: "Сообщение при нажатии"
variable: "Передать переменную с событием"
no-variable: "нет"
callAiScript: "Вызвать AiScript"
_callAiScript:
functionName: "Имя функции"
radioButton: "Кнопка-переключатель"
_radioButton:
name: "Имя переменной"
title: "Заголовок."
default: "Значение по умолчанию"
title: "Заголовок"
values: "Значения"
default: "Исходное значение"
script:
categories:
list: "Списки"
flow: "Управление исполнением"
logical: "Логические"
operation: "Арифметические"
comparison: "Сравнение"
random: "Случайные"
value: "Значения"
fn: "Функции"
text: "Текстовые"
convert: "Преобразование"
list: "Список"
blocks:
text: "Строка текста"
multiLineText: "Многострочный текст"
textList: "Список строк текста"
_textList:
info: "Пишите каждый пункт с новой строки"
strLen: "Длина текста"
_strLen:
arg1: "Текст"
_strReplace:
strPick: "Взять знак из текста"
_strPick:
arg1: "Текст"
arg2: "Позиция знака"
strReplace: "Замена текста"
_strReplace:
arg1: "Текст, в котором заменять"
arg2: "Заменяемый текст"
arg3: "Менять на"
strReverse: "В обратном порядке"
_strReverse:
arg1: "Текст"
join: "Объединение"
_join:
arg1: "Списки"
arg2: "Разделитель"
add: "Добавить"
_add:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
subtract: "Вычитание"
_subtract:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
multiply: "Умножение"
_multiply:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
divide: "Деление"
_divide:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
mod: "Остаток от деления"
_mod:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
round: "Округление до целого"
_round:
arg1: "Число"
eq: "A равно B"
_eq:
arg1: "А"
arg2: "Б"
arg2: "B"
notEq: "A не равно B"
_notEq:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
and: "A и B"
_and:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
or: "A или B"
_or:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
lt: "A < B (меньше)"
_lt:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
gt: "A > B (больше)"
_gt:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
ltEq: "A ⩽ B (меньше или равно)"
_ltEq:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
gtEq: "A ⩾ B (больше или равно)"
_gtEq:
arg1: "А"
arg2: "Б"
arg1: "A"
arg2: "B"
if: "Условный"
_if:
arg1: "Если"
arg1: "Условие"
arg2: "Если правда"
arg3: "Если ложь"
not: "Отрицание"
_not:
arg1: "Условие"
random: "Случайность"
_random:
arg1: "Вероятность"
rannum: "Случайное число"
_rannum:
arg2: "Максимальное значение"
arg1: "Минимум"
arg2: "Максимум"
randomPick: "Случайный выбор из списка"
_randomPick:
arg1: "Списки"
dailyRandom: "Случайность (на день для пользователя)"
_dailyRandom:
arg1: "Вероятность"
dailyRannum: "Случайное число (на день для пользователя)"
_dailyRannum:
arg2: "Максимальное значение"
arg1: "Минимум"
arg2: "Максимум"
dailyRandomPick: "Случайный выбор из списка (на день для пользователя)"
_dailyRandomPick:
arg1: "Списки"
seedRandom: "Псевдослучайность (заданная зерном)"
_seedRandom:
arg1: "Зерно"
arg2: "Вероятность"
seedRannum: "Псевдослучайное число (заданное зерном)"
_seedRannum:
arg3: "Максимальное значение"
arg1: "Зерно"
arg2: "Минимум"
arg3: "Максимум"
seedRandomPick: "Псевдослучайный выбор из списка (заданный зерном)"
_seedRandomPick:
arg1: "Зерно"
arg2: "Списки"
pick: "Выберите из списка"
DRPWPM: "Случайный выбор из взвешенного списка (на день для пользователя)"
_DRPWPM:
arg1: "Список строк текста"
pick: "Выбор из списка"
_pick:
arg1: "Списки"
arg2: "Расположение"
arg2: "Индекс"
listLen: "Количество элементов в списке"
_listLen:
arg1: "Списки"
number: "Номер"
number: "Число"
stringToNumber: "Число из текста"
_stringToNumber:
arg1: "Текст"
numberToString: "Число в текст"
_numberToString:
arg1: "Номер"
arg1: "Число"
splitStrByLine: "Разделение текста на строки"
_splitStrByLine:
arg1: "Текст"
ref: "Переменная"
aiScriptVar: "Переменная AiScript"
fn: "функции"
fn: "Свои функции"
_fn:
slots: "Слоты"
slots: "Аргументы"
slots-info: "Напишите имя каждого аргумента с новой строки"
arg1: "Формула"
for: "Цикл"
_for:
arg1: "Количество повторений"
arg2: "Действие"
typeError: "Аргумент {slot} должен быть иметь тип «{expect}», а передали «{actual}»!"
thereIsEmptySlot: "Аргумент {slot} не заполнен!"
types:
string: "Текст"
number: "Число"
boolean: "Логический"
array: "Списки"
stringArray: "Список строк текста"
emptySlot: "Пустой аргумент"
enviromentVariables: "Переменная окружения"
pageVariables: "Элемент страницы"
argVariables: "Аргументы"
_relayStatus:
requesting: "В ожидании одобрения"
accepted: "Одобрено"
rejected: "Отказано"
_notification:
fileUploaded: "Файл успешно загружен"
youGotMention: "{name} упоминает вас"
youGotReply: "{name} отвечает вам"
youGotQuote: "{name} цитирует вас"
youRenoted: "{name} передаёт вашу заметку"
youGotPoll: "{name} — участник вашего опроса"
youGotMessagingMessageFromUser: "{name} пишет вам"
youGotMessagingMessageFromGroup: "Отправлено сообщение в группу «{name}»"
youWereFollowed: "У вас новый подписчик"
youReceivedFollowRequest: "У вас новый запрос на подписку"
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен"
youWereInvitedToGroup: "Приглашение в группу"
_types:
all: "Все"
follow: "Подписки"
mention: "Упоминание"
renote: "Репост"
quote: "Цитата"
mention: "Упоминания"
reply: "Ответы"
renote: "Репосты"
quote: "Цитаты"
reaction: "Реакции"
pollVote: "Голосования"
receiveFollowRequest: "Получен запрос на подписку"
followRequestAccepted: "Запрос на подписку одобрен"
groupInvited: "Пришлашение в группы"
app: "Уведомления из приложений"
_deck:
alwaysShowMainColumn: "Всегда показывать главную колонку"
columnAlign: "Выравнивание колонн"
columnAlign: "Выравнивание колонок"
addColumn: "Добавить колонку"
swapLeft: "Переместить влево"
swapRight: "Переместить вправо"
swapUp: однять вверх"
swapDown: "Переместить Вниз"
swapLeft: "Переставить левее"
swapRight: "Переставить правее"
swapUp: ереставить выше"
swapDown: "Переставить ниже"
stackLeft: "В столбик влево"
popRight: "Из столбика вправо"
_columns:
widgets: "Виджеты"
notifications: "Уведомления"

View File

@@ -412,6 +412,7 @@ noMessagesYet: "现在没有新的聊天"
newMessageExists: "新信息"
onlyOneFileCanBeAttached: "只能添加一个附件"
signinRequired: "请先登录"
invitations: "邀请"
invitationCode: "邀请码"
checking: "正在确认"
available: "可用"
@@ -593,6 +594,13 @@ fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发
abuseReported: "内容已发送。感谢您的报告。"
send: "发送"
abuseMarkAsResolved: "处理完毕"
openInNewTab: "在新标签页中打开"
openInSideView: "在侧边栏中打开"
defaultNavigationBehaviour: "默认导航"
editTheseSettingsMayBreakAccount: "编辑这些设置可以会损坏您的账号"
random: "随机"
_reversi:
total: "总计"
_serverDisconnectedBehavior:
reload: "自动重载"
dialog: "对话框警告"

View File

@@ -4,10 +4,10 @@ introMisskey: "歡迎! Misskey是一個開源的去中心化的社群網站。
monthAndDay: "{month}月 {day}日"
search: "搜尋"
notifications: "通知"
username: "使用名稱"
username: "使用名稱"
password: "密碼"
fetchingAsApObject: "從 Fediverse 查詢中..."
ok: "確定"
ok: "OK"
gotIt: "知道了"
cancel: "取消"
enterUsername: "輸入使用者名稱"
@@ -121,9 +121,9 @@ emojiUrl: "表情符號URL"
addEmoji: "新增表情符號"
settingGuide: "推薦設定"
cacheRemoteFiles: "緩存非遠程檔案"
cacheRemoteFilesDescription: "如果禁用此設定,遠程文件將會被直接連結而非緩存。禁用將節省服務器上的存儲空間,但會因為沒有生成預覽圖而增加流量。"
flagAsBot: "此帳戶是Bot"
flagAsCat: "此帳戶是Cat"
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間。但資料會因直接連線從而產生額外連接數據。"
flagAsBot: "此使用者是機器人"
flagAsCat: "此使用者是貓"
autoAcceptFollowed: "自動許可追隨"
addAcount: "新增帳號"
loginFailed: "登入失敗"
@@ -227,6 +227,7 @@ messageRead: "已讀"
noMoreHistory: "沒有更多歷史紀錄"
startMessaging: "開始傳送訊息"
nUsersRead: "{n}人已讀"
agreeTo: "我同意{0}"
tos: "使用條款"
start: "開始"
home: "首頁"
@@ -408,6 +409,7 @@ noMessagesYet: "沒有訊息"
newMessageExists: "有新的訊息"
onlyOneFileCanBeAttached: "只能添加一個附件"
signinRequired: "請先登入"
invitations: "邀請"
invitationCode: "邀請碼"
checking: "確認中"
available: "可用的"
@@ -444,9 +446,20 @@ openImageInNewTab: "於新分頁中開啟圖片"
local: "本地"
remote: "遠端"
total: "合計"
weekOverWeekChanges: "與上週相比"
dayOverDayChanges: "與前一日相比"
appearance: "外觀"
accountSettings: "帳戶設置"
clientSettings: "用戶端設定"
accountSettings: "帳號設定"
promotion: "推廣貼文"
numberOfDays: "有效天數"
hideThisNote: "隱藏此貼文"
objectStorageBaseUrl: "Base URL"
objectStorageBucket: "儲存空間Bucket"
objectStoragePrefix: "前綴"
objectStorageEndpoint: "訪問網域名稱Endpoint"
objectStorageEndpointDesc: "如要使用AWS S3請留空。否則請根據伺服器要求以'<host>'或 '<host>:<port>'的形式設定訪問網域名稱Endpoint。"
objectStorageRegion: "地域Region"
objectStorageUseSSL: "使用SSL"
objectStorageUseProxy: "使用網路代理"
serverLogs: "伺服器日誌"
@@ -512,24 +525,37 @@ tokenRequested: "允許訪問帳號"
notificationType: "通知形式"
edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號"
emailConfig: "電郵服務器設定"
emailConfig: "電子郵件伺服器設定"
enableEmail: "啟用發送電郵功能"
emailConfigInfo: "用於確認電郵地址及密碼重置"
email: "電郵地址"
smtpConfig: "SMTP服器設定"
smtpConfig: "SMTP服器設定"
smtpHost: "主機"
smtpPort: "端口"
smtpUser: "使用名稱"
smtpUser: "使用名稱"
smtpPass: "密碼"
emptyToDisableSmtpAuth: "留空使用者名稱和密碼以禁用SMTP驗證。"
testEmail: "郵件測試發送"
display: "檢視"
copy: "複製"
metrics: "指標"
overview: "概覽"
logs: "日誌"
delayed: "延遲"
database: "資料庫"
channel: "頻道"
create: "新增"
notificationSetting: "管理通知"
notificationSetting: "通知設定"
other: "其他"
sample: "範例 "
abuseReports: "檢舉"
reportAbuse: "檢舉"
reportAbuseOf: "檢舉{name}"
send: "發送"
openInNewTab: "在新分頁中開啟"
random: "隨機"
_reversi:
total: "合計"
_serverDisconnectedBehavior:
reload: "自動重載"
dialog: "以對話框警告"
@@ -537,7 +563,7 @@ _serverDisconnectedBehavior:
_channel:
create: "建立頻道"
edit: "編輯頻道"
setBanner: "設置封面圖"
setBanner: "設定橫幅"
removeBanner: "移除封面圖"
featured: "流行"
owned: "管理中"
@@ -550,15 +576,19 @@ _sidebar:
_wordMute:
muteWords: "加入靜音文字"
_theme:
constant: "常數"
color: "顏色"
func: "函数"
keys:
bg: "背景"
fg: "文本"
mention: "提及"
renote: "轉發貼文"
divider: "分割線"
infoBg: "資訊背景"
infoFg: "資訊內容"
infoWarnBg: "警告背景"
infoWarnFg: "警告字元"
_sfx:
note: "貼文"
noteMy: "我的貼文"
@@ -595,6 +625,7 @@ _tutorial:
step4_1: "筆記發出去了嗎?"
step4_2: "如果你的貼文有顯示在時間軸上,就代表已經發文成功。"
step5_1: "現在試試看追隨其他人來讓你的時間軸變得更生動吧。"
step5_2: "你可以在{featured}上看到受歡迎的貼文,你也可以選擇從列表中追隨你喜歡的人,或者在{explore}上找到熱門使用者。"
step5_3: "想要追隨其他人,只要點擊他們的頭像並按「追隨」即可。"
step5_4: "如果使用者的名字旁有鎖頭的圖示,代表他們需要手動核准你的追隨請求。"
step6_1: "現在你可以在時間軸上看到其他用戶的貼文"
@@ -615,10 +646,16 @@ _permissions:
"read:reactions": "查看反應"
"write:reactions": "編輯反應"
"write:votes": "投票"
"read:user-groups": "顯示使用者群組"
"write:user-groups": "編輯使用者群組"
"read:channels": "已查看的頻道"
"write:channels": "操作頻道"
_antennaSources:
all: "全部貼文"
homeTimeline: "來自已追隨使用者的貼文"
users: "來自特定使用者的貼文"
userList: "來自特定清單中的貼文"
userGroup: "來自特定群組的貼文"
_weekday:
sunday: "週日"
monday: "週一"
@@ -657,14 +694,30 @@ _poll:
voted: "已投票"
closed: "已結束"
_visibility:
public: "公開"
home: "首頁"
followers: "追隨者"
specified: "指定使用者"
specifiedDescription: "僅發送至指定使用者"
localOnly: "僅限本地"
localOnlyDescription: "對遠端使用者隱藏"
_postForm:
replyPlaceholder: "回覆此貼文..."
quotePlaceholder: "引用此貼文..."
channelPlaceholder: "發佈到頻道"
_placeholders:
a: "今天過得如何?"
b: "有什麼新鮮事嗎?"
c: "有什麼新鮮想法嗎?"
d: "想要發布些什麼嗎?"
e: "寫些什麼吧..."
f: "期待你發佈的內容..."
_profile:
name: "名稱"
username: "使用名稱"
username: "使用名稱"
description: "關於我"
youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag"
metadata: "更多資訊"
metadataLabel: "標籤"
metadataContent: "内容"
_exportOrImport:
@@ -674,42 +727,158 @@ _exportOrImport:
blockingList: "封鎖"
userLists: "清單"
_charts:
usersIncDec: "使用者増減"
usersTotal: "使用者合共"
activeUsers: "活躍使用者"
notesIncDec: "貼文増減"
localNotesIncDec: "本地貼文増減"
remoteNotesIncDec: "非本地貼文的數目增减"
notesTotal: "貼文合共"
filesIncDec: "檔案増減"
filesTotal: "累計檔案"
storageUsageIncDec: "儲存空間的増減"
storageUsageTotal: "已使用的儲存空間合共"
_instanceCharts:
requests: "請求"
users: "使用者増減"
usersTotal: "總計使用者"
notes: "貼文増減"
notesTotal: "累計貼文"
ff: "追隨/追隨者的増減"
ffTotal: "追隨/追隨者累計"
cacheSize: "增加或減少快取用量"
cacheSizeTotal: "快取大小總計"
files: "檔案數量的増減"
filesTotal: "檔案數量總計"
_timelines:
home: "首頁"
local: "本地"
social: "社群"
global: "全域"
_rooms:
roomOf: "{user}的房間"
addFurniture: "擺放家具"
translate: "移動 "
rotate: "旋轉"
exit: "返回"
remove: "移除"
clear: "全部移除"
clearConfirm: "確定要移除全部家具嗎?"
leaveConfirm: "修改未儲存,是否要離開?"
chooseImage: "選擇圖像"
roomType: "房間種類"
carpetColor: "地板顏色"
_roomType:
default: "預設"
washitsu: "和室"
_furnitures:
milk: "牛奶盒"
bed: "床"
low-table: "咖啡桌"
desk: "書桌"
chair: "椅子"
chair2: "椅子2"
fan: "通風機"
pc: "電腦"
plant: "觀葉植物"
plant2: "觀葉植物2"
eraser: "橡皮擦"
pencil: "鉛筆"
pudding: "布丁"
cardboard-box: "紙板箱"
cardboard-box2: "紙板箱2"
cardboard-box3: "紙板箱3"
book: "讀物"
book2: "讀物2"
piano: "鋼琴"
moon: "月亮"
corkboard: "木栓板"
mousepad: "滑鼠墊"
monitor: "監視器"
keyboard: "鍵盤"
carpet-stripe: "條紋地毯"
bin: "垃圾箱"
cup-noodle: "杯面"
holo-display: "投影機"
energy-drink: "能量飲料"
doll-ai: "小藍的人偶公仔"
banknote: "大疊鈔票"
_pages:
newPage: "建立頁面"
editPage: "編輯頁面"
created: "頁面已建立"
updated: "頁面已更新"
deleted: "頁面已被刪除"
editThisPage: "編輯此頁面"
viewSource: "檢視原始碼"
viewPage: "顯示頁面"
like: "喜歡"
unlike: "收回喜歡"
my: "我的頁面"
liked: "已喜歡的頁面"
inspector: "面板檢查"
variables: "變數"
title: "標題"
url: "頁面網址"
fontSerif: "襯線體"
fontSansSerif: "無襯線體"
inputBlocks: "輸入"
blocks:
section: "區段"
image: "圖片"
button: "按鈕"
if: "如果"
_if:
variable: "變數"
_post:
text: "内容"
_textInput:
name: "變數名稱"
text: "標題"
_textareaInput:
name: "變數名稱"
text: "標題"
numberInput: "輸入數值"
_numberInput:
name: "變數名稱"
text: "標題"
_canvas:
width: "寬度"
_counter:
height: "高度"
switch: "開關"
_switch:
name: "變數名稱"
text: "標題"
default: "預設值"
counter: "計數器"
_counter:
name: "變數名稱"
text: "標題"
inc: "増加値"
_button:
text: "標題"
colored: "彩色"
_action:
_dialog:
content: "内容"
resetRandom: "重設亂數"
pushEvent: "發送事件"
_pushEvent:
event: "事件名稱"
no-variable: "沒有"
callAiScript: "調用AiScript"
_callAiScript:
functionName: "函數名稱"
radioButton: "選項"
_radioButton:
name: "變數名稱"
title: "標題"
script:
categories:
logical: "邏輯運算"
operation: "計算"
comparison: "對比"
random: "隨機"
value: "數值 "
fn: "函数"
text: "文本操作"
@@ -732,18 +901,22 @@ _pages:
_add:
arg1: "A"
arg2: "B"
subtract: "减去"
_subtract:
arg1: "A"
arg2: "B"
multiply: "乘"
_multiply:
arg1: "A"
arg2: "B"
divide: "除"
_divide:
arg1: "A"
arg2: "B"
_mod:
arg1: "A"
arg2: "B"
round: "四舍五入"
_round:
arg1: "數值"
eq: "A和B相等"
@@ -785,6 +958,7 @@ _pages:
not: "否"
_not:
arg1: "否"
random: "隨機"
_random:
arg1: "機率"
rannum: "亂數"
@@ -853,15 +1027,25 @@ _notification:
yourFollowRequestAccepted: "您的追隨請求已通過"
youWereInvitedToGroup: "您有新的群組邀請"
_types:
all: "全部 "
follow: "追隨中"
mention: "提及"
reply: "回覆"
renote: "轉發貼文"
quote: "引用"
reaction: "反應"
_deck:
swapLeft: "向左移動"
swapRight: "向右移動"
swapUp: "往上移動"
swapDown: "往下移動"
stackLeft: "向左折疊"
popRight: "向右彈出"
_columns:
widgets: "小工具"
notifications: "通知"
tl: "時間軸"
antenna: "天線"
list: "清單"
mentions: "提及"
direct: "指定使用者"

View File

@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instanceThemeColor1603776877564 implements MigrationInterface {
name = 'instanceThemeColor1603776877564'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" ADD "themeColor" character varying(64) DEFAULT null`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "themeColor"`);
}
}

View File

@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instanceFavicon1603781553011 implements MigrationInterface {
name = 'instanceFavicon1603781553011'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" ADD "faviconUrl" character varying(256) DEFAULT null`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "faviconUrl"`);
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.50.0",
"version": "12.51.0",
"codename": "indigo",
"repository": {
"type": "git",

View File

@@ -4,7 +4,7 @@
<div class="icon" v-if="icon">
<Fa :icon="icon"/>
</div>
<div class="icon" v-else-if="!input && !select && !user" :class="type">
<div class="icon" v-else-if="!input && !select" :class="type">
<Fa :icon="faCheck" v-if="type === 'success'"/>
<Fa :icon="faTimesCircle" v-if="type === 'error'"/>
<Fa :icon="faExclamationTriangle" v-if="type === 'warning'"/>
@@ -12,11 +12,9 @@
<Fa :icon="faQuestionCircle" v-if="type === 'question'"/>
<Fa :icon="faSpinner" pulse v-if="type === 'waiting'"/>
</div>
<header v-if="title" v-html="title"></header>
<header v-if="title == null && user">{{ $t('enterUsername') }}</header>
<div class="body" v-if="text" v-html="text"></div>
<header v-if="title"><Mfm :text="title"/></header>
<div class="body" v-if="text"><Mfm :text="text"/></div>
<MkInput v-if="input" v-model:value="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput>
<MkInput v-if="user" v-model:value="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></MkInput>
<MkSelect v-if="select" v-model:value="selectedValue" autofocus>
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
@@ -28,8 +26,8 @@
</template>
</MkSelect>
<div class="buttons" v-if="(showOkButton || showCancelButton) && !actions">
<MkButton inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</MkButton>
<MkButton inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</MkButton>
<MkButton inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select">{{ (showCancelButton || input || select) ? $t('ok') : $t('gotIt') }}</MkButton>
<MkButton inline @click="cancel" v-if="showCancelButton || input || select">{{ $t('cancel') }}</MkButton>
</div>
<div class="buttons" v-if="actions">
<MkButton v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</MkButton>
@@ -46,8 +44,6 @@ import MkModal from '@/components/ui/modal.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkSelect from '@/components/ui/select.vue';
import parseAcct from '../../misc/acct/parse';
import * as os from '@/os';
export default defineComponent({
components: {
@@ -77,9 +73,6 @@ export default defineComponent({
select: {
required: false
},
user: {
required: false
},
icon: {
required: false
},
@@ -105,28 +98,12 @@ export default defineComponent({
data() {
return {
inputValue: this.input && this.input.default ? this.input.default : null,
userInputValue: null,
selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
canOk: true,
faTimesCircle, faQuestionCircle, faSpinner, faInfoCircle, faExclamationTriangle, faCheck
};
},
watch: {
userInputValue() {
if (this.user) {
os.api('users/show', parseAcct(this.userInputValue)).then(u => {
this.canOk = u != null;
}).catch(() => {
this.canOk = false;
});
}
},
},
mounted() {
if (this.user) this.canOk = false;
document.addEventListener('keydown', this.onKeydown);
},
@@ -141,21 +118,13 @@ export default defineComponent({
},
async ok() {
if (!this.canOk) return;
if (!this.showOkButton) return;
if (this.user) {
const user = await os.api('users/show', parseAcct(this.userInputValue));
if (user) {
this.done(false, user);
}
} else {
const result =
this.input ? this.inputValue :
this.select ? this.selectedValue :
true;
this.done(false, result);
}
const result =
this.input ? this.inputValue :
this.select ? this.selectedValue :
true;
this.done(false, result);
},
cancel() {

View File

@@ -185,7 +185,7 @@ export default defineComponent({
transition: color 0.2s ease;
&:hover {
color: var(--textHighlighted);
color: var(--fgHighlighted);
transition: color 0s;
}

View File

@@ -9,7 +9,6 @@
import { defineComponent } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { twemojiSvgBase } from '../../misc/twemoji-base';
import * as os from '@/os';
export default defineComponent({
props: {

View File

@@ -0,0 +1,62 @@
<template>
<div class="hpaizdrt" :style="bg">
<img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/>
<span class="name">{{ info.name }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { instanceName } from '@/config';
export default defineComponent({
props: {
instance: {
type: Object,
required: false
},
},
data() {
return {
info: this.instance || {
faviconUrl: '/favicon.ico',
name: instanceName,
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
}
}
},
computed: {
bg(): any {
const themeColor = this.info.themeColor || '#777777';
return {
background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
};
}
}
});
</script>
<style lang="scss" scoped>
.hpaizdrt {
$height: 1.1rem;
height: $height;
border-radius: 4px 0 0 4px;
overflow: hidden;
color: #fff;
> .icon {
height: 100%;
}
> .name {
margin-left: 4px;
line-height: $height;
font-size: 0.9em;
vertical-align: top;
font-weight: bold;
}
}
</style>

View File

@@ -40,6 +40,7 @@
<MkAvatar class="avatar" :user="appearNote.user"/>
<div class="main">
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
<div class="body" ref="noteBody">
<p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
@@ -139,6 +140,7 @@ export default defineComponent({
XCwButton,
XPoll,
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
},
inject: {
@@ -258,6 +260,12 @@ export default defineComponent({
} else {
return null;
}
},
showTicker() {
if (this.$store.state.device.instanceTicker === 'always') return true;
if (this.$store.state.device.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
return false;
}
},

View File

@@ -20,7 +20,7 @@
<header>
<MkA v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></MkA>
<span v-else>{{ notification.header }}</span>
<MkTime :time="notification.createdAt" v-if="withTime"/>
<MkTime :time="notification.createdAt" v-if="withTime" class="time"/>
</header>
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Fa :icon="faQuoteLeft"/>
@@ -260,7 +260,7 @@ export default defineComponent({
overflow: hidden;
}
> .mk-time {
> .time {
margin-left: auto;
font-size: 0.9em;
}

View File

@@ -562,6 +562,10 @@ export default defineComponent({
});
}).catch(err => {
this.posting = false;
os.dialog({
type: 'error',
text: err.message + '<br>' + (err as any).id,
});
});
},

View File

@@ -3,7 +3,7 @@
<div class="bqxuuuey">
<div class="info">
<div>{{ reaction.replace('@.', '') }}</div>
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon"/>
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
</div>
<template v-if="users.length <= 10">
<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
@@ -66,7 +66,6 @@ export default defineComponent({
> .icon {
display: block;
width: 60px;
height: 60px;
margin: 0 auto;
}
}

View File

@@ -246,7 +246,7 @@ export default defineComponent({
icon: faQuestionCircle,
}, {
type: 'link',
text: this.$t('aboutX', { x: instanceName || host }),
text: this.$t('aboutX', { x: instanceName }),
to: '/about',
icon: faInfoCircle,
}, {

View File

@@ -1,5 +1,5 @@
<template>
<time class="mk-time" :title="absolute">
<time :title="absolute">
<template v-if="mode == 'relative'">{{ relative }}</template>
<template v-else-if="mode == 'absolute'">{{ absolute }}</template>
<template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template>

View File

@@ -31,6 +31,10 @@ export default defineComponent({
type: String,
required: false,
},
behavior: {
type: String,
required: false,
},
},
computed: {
@@ -84,6 +88,13 @@ export default defineComponent({
},
nav() {
if (this.behavior) {
if (this.behavior === 'window') {
os.pageWindow(this.to);
return;
}
}
if (this.navHook) {
this.navHook(this.to);
} else {

View File

@@ -1,7 +1,9 @@
<template>
<div class="nvlagfpb" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" @close="$emit('closed')" class="_popup _shadow" :align="'left'"/>
</div>
<transition :name="$store.state.device.animation ? 'fade' : ''" appear>
<div class="nvlagfpb" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" @close="$emit('closed')" class="_popup _shadow" :align="'left'"/>
</div>
</transition>
</template>
<script lang="ts">
@@ -35,8 +37,30 @@ export default defineComponent({
},
},
mounted() {
this.$el.style.top = this.ev.pageY + 'px';
this.$el.style.left = this.ev.pageX + 'px';
let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
const width = this.$el.offsetWidth;
const height = this.$el.offsetHeight;
if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset;
}
if (top + height - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - height + window.pageYOffset;
}
if (top < 0) {
top = 0;
}
if (left < 0) {
left = 0;
}
this.$el.style.top = top + 'px';
this.$el.style.left = left + 'px';
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', this.onMousedown);
@@ -60,4 +84,14 @@ export default defineComponent({
position: absolute;
z-index: 65535;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
transform-origin: left top;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
transform: scale(0.9);
}
</style>

View File

@@ -132,7 +132,7 @@ export default defineComponent({
&.max-width_500px {
> header {
> .title {
padding: 8px 10px;
padding: 8px 10px 8px 0;
}
}
}

View File

@@ -175,7 +175,7 @@ export default defineComponent({
}
.modal-popup-content-enter-active, .modal-popup-content-leave-active {
transition: opacity 0.3s, transform 0.3s !important;
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
.modal-popup-content-enter-from, .modal-popup-content-leave-to {
pointer-events: none;

View File

@@ -52,6 +52,7 @@ export default defineComponent({
position: relative;
display: inline-block;
margin: 16px 32px 0 0;
text-align: left;
cursor: pointer;
transition: all 0.3s;

View File

@@ -96,7 +96,7 @@ export default defineComponent({
margin: 0;
line-height: 16px;
font-size: 0.8em;
color: var(--text);
color: var(--fg);
opacity: 0.7;
}
}
@@ -125,7 +125,7 @@ export default defineComponent({
> p {
margin: 0;
font-size: 0.7em;
color: var(--text);
color: var(--fg);
}
> span {

View File

@@ -151,7 +151,7 @@ export default defineComponent({
margin: 0;
line-height: 16px;
font-size: 0.8em;
color: var(--text);
color: var(--fg);
opacity: 0.7;
}
}
@@ -159,7 +159,7 @@ export default defineComponent({
> .description {
padding: 0 16px;
font-size: 0.8em;
color: var(--text);
color: var(--fg);
}
> .status {
@@ -172,7 +172,7 @@ export default defineComponent({
> p {
margin: 0;
font-size: 0.7em;
color: var(--text);
color: var(--fg);
}
> span {

View File

@@ -12,5 +12,5 @@ export const lang = localStorage.getItem('lang');
export const langs = _LANGS_;
export const getLocale = async () => Object.fromEntries((await entries(clientDb.i18n)) as [string, string][]);
export const version = _VERSION_;
export const instanceName = siteName === 'Misskey' ? null : siteName;
export const instanceName = siteName === 'Misskey' ? host : siteName;
export const deckmode = localStorage.getItem('deckmode') === 'true';

View File

@@ -10,7 +10,7 @@ import { resolve } from '@/router';
const ua = navigator.userAgent.toLowerCase();
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
export const stream = new Stream();
export const stream = markRaw(new Stream());
export const pendingApiRequestsCount = ref(0);

View File

@@ -0,0 +1,96 @@
<template>
<div>
<section class="_section">
<MkInput v-model:value="endpoint" :datalist="endpoints" @update:value="onEndpointChange()">
<span>Endpoint</span>
</MkInput>
<MkTextarea v-model:value="body" code>
<span>Params (JSON or JSON5)</span>
</MkTextarea>
<MkSwitch v-model:value="withCredential">
With credential
</MkSwitch>
<MkButton primary full @click="send" :disabled="sending">
<template v-if="sending"><MkEllipsis/></template>
<template v-else><Fa :icon="faPaperPlane"/> Send</template>
</MkButton>
</section>
<section class="_section" v-if="res">
<MkTextarea v-model:value="res" code readonly tall>
<span>Response</span>
</MkTextarea>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faTerminal, faPaperPlane } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton, MkInput, MkTextarea, MkSwitch,
},
data() {
return {
INFO: {
header: [{
title: 'API console',
icon: faTerminal
}]
},
endpoint: '',
body: '{}',
res: null,
sending: false,
endpoints: [],
withCredential: true,
faPaperPlane
};
},
created() {
os.api('endpoints').then(endpoints => {
this.endpoints = endpoints;
});
},
methods: {
send() {
this.sending = true;
os.api(this.endpoint, JSON5.parse(this.body)).then(res => {
this.sending = false;
this.res = JSON5.stringify(res, null, 2);
}, err => {
this.sending = false;
this.res = JSON5.stringify(err, null, 2);
});
},
onEndpointChange() {
os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => {
const body = {};
for (const p of endpoint.params) {
body[p.name] =
p.type === 'String' ? '' :
p.type === 'Number' ? 0 :
p.type === 'Boolean' ? false :
p.type === 'Array' ? [] :
p.type === 'Object' ? {} :
null;
}
this.body = JSON5.stringify(body, null, 2);
});
}
}
});
</script>

View File

@@ -15,12 +15,12 @@
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
<header v-if="message.groupId">
<span class="name">{{ message.group.name }}</span>
<MkTime :time="message.createdAt"/>
<MkTime :time="message.createdAt" class="time"/>
</header>
<header v-else>
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
<MkTime :time="message.createdAt"/>
<MkTime :time="message.createdAt" class="time"/>
</header>
<div class="body">
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
@@ -50,8 +50,6 @@ export default defineComponent({
MkButton
},
inject: ['navHook'],
data() {
return {
INFO: {
@@ -245,7 +243,7 @@ export default defineComponent({
margin: 0 8px;
}
> .mk-time {
> .time {
margin: 0 0 0 auto;
}
}

View File

@@ -0,0 +1,472 @@
<template>
<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
<header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ $t('_reversi.black') }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ $t('_reversi.white') }})</header>
<div style="overflow: hidden; line-height: 28px;">
<p class="turn" v-if="!iAmPlayer && !game.isEnded">
<Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
<MkEllipsis/>
</p>
<p class="turn" v-if="logPos != logs.length">
<Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
</p>
<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn()">{{ $t('_reversi.opponentTurn') }}<MkEllipsis/></p>
<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn()" style="animation: anime-tada 1s linear infinite both;">{{ $t('_reversi.myTurn') }}</p>
<p class="result" v-if="game.isEnded && logPos == logs.length">
<template v-if="game.winner">
<Mfm :key="'won'" :text="$t('_reversi.won', { name: game.winner.name })" :plain="true" :custom-emojis="game.winner.emojis"/>
<span v-if="game.surrendered != null"> ({{ $t('_reversi.surrendered') }})</span>
</template>
<template v-else>{{ $t('_reversi.drawn') }}</template>
</p>
</div>
<div class="board">
<div class="labels-x" v-if="$store.state.settings.gamesReversiShowBoardLabels">
<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
</div>
<div class="flex">
<div class="labels-y" v-if="$store.state.settings.gamesReversiShowBoardLabels">
<div v-for="i in game.map.length">{{ i }}</div>
</div>
<div class="cells" :style="cellsStyle">
<div v-for="(stone, i) in o.board"
:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn(), can: turnUser() ? o.canPut(turnUser().id == blackUser.id, i) : null, prev: o.prevPos == i }"
@click="set(i)"
:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"
>
<template v-if="$store.state.settings.gamesReversiUseAvatarStones || true">
<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
</template>
<template v-else>
<fa v-if="stone === true" :icon="fasCircle"/>
<fa v-if="stone === false" :icon="farCircle"/>
</template>
</div>
</div>
<div class="labels-y" v-if="$store.state.settings.gamesReversiShowBoardLabels">
<div v-for="i in game.map.length">{{ i }}</div>
</div>
</div>
<div class="labels-x" v-if="$store.state.settings.gamesReversiShowBoardLabels">
<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
</div>
</div>
<p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $t('_reversi.black') }}:{{ o.blackCount }} {{ $t('_reversi.white') }}:{{ o.whiteCount }} {{ $t('_reversi.total') }}:{{ o.blackCount + o.whiteCount }}</p>
<div class="actions" v-if="!game.isEnded && iAmPlayer">
<MkButton @click="surrender">{{ $t('_reversi.surrender') }}</MkButton>
</div>
<div class="player" v-if="game.isEnded">
<span>{{ logPos }} / {{ logs.length }}</span>
<!-- TODO <ui-horizon-group> -->
<MkButton @click="logPos = 0" :disabled="logPos == 0"><fa :icon="faAngleDoubleLeft"/></MkButton>
<MkButton @click="logPos--" :disabled="logPos == 0"><fa :icon="faAngleLeft"/></MkButton>
<MkButton @click="logPos++" :disabled="logPos == logs.length"><fa :icon="faAngleRight"/></MkButton>
<MkButton @click="logPos = logs.length" :disabled="logPos == logs.length"><fa :icon="faAngleDoubleRight"/></MkButton>
<!-- TODO </ui-horizon-group> -->
</div>
<div class="info">
<p v-if="game.isLlotheo">{{ $t('_reversi.isLlotheo') }}</p>
<p v-if="game.loopedBoard">{{ $t('_reversi.loopedMap') }}</p>
<p v-if="game.canPutEverywhere">{{ $t('_reversi.canPutEverywhere') }}</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
import * as CRC32 from 'crc-32';
import Reversi, { Color } from '../../../games/reversi/core';
import { url } from '@/config';
import MkButton from '@/components/ui/button.vue';
import { userPage } from '@/filters/user';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton
},
props: {
initGame: {
type: Object,
require: true
},
connection: {
type: Object,
require: true
},
},
data() {
return {
game: JSON.parse(JSON.stringify(this.initGame)),
o: null as Reversi,
logs: [],
logPos: 0,
pollingClock: null,
faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight, fasCircle, farCircle
};
},
computed: {
iAmPlayer(): boolean {
if (!this.$store.getters.isSignedIn) return false;
return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id;
},
myColor(): Color {
if (!this.iAmPlayer) return null;
if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true;
if (this.game.user2Id == this.$store.state.i.id && this.game.black == 2) return true;
return false;
},
opColor(): Color {
if (!this.iAmPlayer) return null;
return this.myColor === true ? false : true;
},
blackUser(): any {
return this.game.black == 1 ? this.game.user1 : this.game.user2;
},
whiteUser(): any {
return this.game.black == 1 ? this.game.user2 : this.game.user1;
},
cellsStyle(): any {
return {
'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
};
}
},
watch: {
logPos(v) {
if (!this.game.isEnded) return;
const o = new Reversi(this.game.map, {
isLlotheo: this.game.isLlotheo,
canPutEverywhere: this.game.canPutEverywhere,
loopedBoard: this.game.loopedBoard
});
for (const log of this.logs.slice(0, v)) {
o.put(log.color, log.pos);
}
this.o = o;
//this.$forceUpdate();
}
},
created() {
this.o = new Reversi(this.game.map, {
isLlotheo: this.game.isLlotheo,
canPutEverywhere: this.game.canPutEverywhere,
loopedBoard: this.game.loopedBoard
});
for (const log of this.game.logs) {
this.o.put(log.color, log.pos);
}
this.logs = this.game.logs;
this.logPos = this.logs.length;
// 通信を取りこぼしてもいいように定期的にポーリングさせる
if (this.game.isStarted && !this.game.isEnded) {
this.pollingClock = setInterval(() => {
if (this.game.isEnded) return;
const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
this.connection.send('check', {
crc32: crc32
});
}, 3000);
}
},
mounted() {
this.connection.on('set', this.onSet);
this.connection.on('rescue', this.onRescue);
this.connection.on('ended', this.onEnded);
},
beforeUnmount() {
this.connection.off('set', this.onSet);
this.connection.off('rescue', this.onRescue);
this.connection.off('ended', this.onEnded);
clearInterval(this.pollingClock);
},
methods: {
userPage,
// this.o がリアクティブになった折にはcomputedにできる
turnUser(): any {
if (this.o.turn === true) {
return this.game.black == 1 ? this.game.user1 : this.game.user2;
} else if (this.o.turn === false) {
return this.game.black == 1 ? this.game.user2 : this.game.user1;
} else {
return null;
}
},
// this.o がリアクティブになった折にはcomputedにできる
isMyTurn(): boolean {
if (!this.iAmPlayer) return false;
if (this.turnUser() == null) return false;
return this.turnUser().id == this.$store.state.i.id;
},
set(pos) {
if (this.game.isEnded) return;
if (!this.iAmPlayer) return;
if (!this.isMyTurn) return;
if (!this.o.canPut(this.myColor, pos)) return;
this.o.put(this.myColor, pos);
// サウンドを再生する
if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/reversi-put-me.mp3`);
sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
this.connection.send('set', {
pos: pos
});
this.checkEnd();
this.$forceUpdate();
},
onSet(x) {
this.logs.push(x);
this.logPos++;
this.o.put(x.color, x.pos);
this.checkEnd();
this.$forceUpdate();
// サウンドを再生する
if (this.$store.state.device.enableSounds && x.color != this.myColor) {
const sound = new Audio(`${url}/assets/reversi-put-you.mp3`);
sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
},
onEnded(x) {
this.game = JSON.parse(JSON.stringify(x.game));
},
checkEnd() {
this.game.isEnded = this.o.isEnded;
if (this.game.isEnded) {
if (this.o.winner === true) {
this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
} else if (this.o.winner === false) {
this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
} else {
this.game.winnerId = null;
this.game.winner = null;
}
}
},
// 正しいゲーム情報が送られてきたとき
onRescue(game) {
this.game = JSON.parse(JSON.stringify(game));
this.o = new Reversi(this.game.map, {
isLlotheo: this.game.isLlotheo,
canPutEverywhere: this.game.canPutEverywhere,
loopedBoard: this.game.loopedBoard
});
for (const log of this.game.logs) {
this.o.put(log.color, log.pos, true);
}
this.logs = this.game.logs;
this.logPos = this.logs.length;
this.checkEnd();
this.$forceUpdate();
},
surrender() {
os.api('games/reversi/games/surrender', {
gameId: this.game.id
});
},
}
});
</script>
<style lang="scss" scoped>
.xqnhankfuuilcwvhgsopeqncafzsquya {
text-align: center;
> .go-index {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 42px;
height :42px;
}
> header {
padding: 8px;
border-bottom: dashed 1px var(--divider);
}
> .board {
width: calc(100% - 16px);
max-width: 500px;
margin: 0 auto;
$label-size: 16px;
$gap: 4px;
> .labels-x {
height: $label-size;
padding: 0 $label-size;
display: flex;
> * {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8em;
&:first-child {
margin-left: -($gap / 2);
}
&:last-child {
margin-right: -($gap / 2);
}
}
}
> .flex {
display: flex;
> .labels-y {
width: $label-size;
display: flex;
flex-direction: column;
> * {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
&:first-child {
margin-top: -($gap / 2);
}
&:last-child {
margin-bottom: -($gap / 2);
}
}
}
> .cells {
flex: 1;
display: grid;
grid-gap: $gap;
> div {
background: transparent;
border-radius: 6px;
overflow: hidden;
* {
pointer-events: none;
user-select: none;
}
&.empty {
border: solid 2px var(--divider);
}
&.empty.can {
border-color: var(--accent);
}
&.empty.myTurn {
border-color: var(--divider);
&.can {
border-color: var(--accent);
cursor: pointer;
&:hover {
background: var(--accent);
}
}
}
&.prev {
box-shadow: 0 0 0 4px var(--accent);
}
&.isEnded {
border-color: var(--divider);
}
&.none {
border-color: transparent !important;
}
> svg, > img {
display: block;
width: 100%;
height: 100%;
}
}
}
}
}
> .status {
margin: 0;
padding: 16px 0;
}
> .actions {
padding-bottom: 16px;
}
> .player {
padding: 0 16px 32px 16px;
margin: 0 auto;
max-width: 500px;
> span {
display: inline-block;
margin: 0 8px;
min-width: 70px;
}
}
}
</style>

View File

@@ -0,0 +1,393 @@
<template>
<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
<header><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></header>
<div>
<p>{{ $t('_reversi.gameSettings') }}</p>
<div class="card map _panel">
<header>
<select v-model="mapName" :placeholder="$t('_reversi.chooseBoard')" @change="onMapChange">
<option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/>
<option :label="$t('random')" :value="null"/>
<optgroup v-for="c in mapCategories" :key="c" :label="c">
<option v-for="m in Object.values(maps).filter(m => m.category == c)" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
</optgroup>
</select>
</header>
<div>
<div class="random" v-if="game.map == null"><fa icon="dice"/></div>
<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
<div v-for="(x, i) in game.map.join('')" :class="{ none: x == ' ' }" @click="onPixelClick(i, x)">
<fa v-if="x == 'b'" :icon="fasCircle"/>
<fa v-if="x == 'w'" :icon="farCircle"/>
</div>
</div>
</div>
</div>
<div class="card _panel">
<header>
<span>{{ $t('_reversi.blackOrWhite') }}</span>
</header>
<div>
<MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $t('random') }}</MkRadio>
<MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
<i18n-t keypath="_reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user1"/></b>
</template>
</i18n-t>
</MkRadio>
<MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
<i18n-t keypath="_reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user2"/></b>
</template>
</i18n-t>
</MkRadio>
</div>
</div>
<div class="card _panel">
<header>
<span>{{ $t('_reversi.rules') }}</span>
</header>
<div>
<MkSwitch v-model:value="game.isLlotheo" @update:value="updateSettings('isLlotheo')">{{ $t('_reversi.isLlotheo') }}</MkSwitch>
<MkSwitch v-model:value="game.loopedBoard" @update:value="updateSettings('loopedBoard')">{{ $t('_reversi.loopedMap') }}</MkSwitch>
<MkSwitch v-model:value="game.canPutEverywhere" @update:value="updateSettings('canPutEverywhere')">{{ $t('_reversi.canPutEverywhere') }}</MkSwitch>
</div>
</div>
<div class="card form _panel" v-if="form">
<header>
<span>{{ $t('_reversi.botSettings') }}</span>
</header>
<div>
<template v-for="item in form">
<MkSwitch v-if="item.type == 'switch'" v-model:value="item.value" :key="item.id" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch>
<div class="card" v-if="item.type == 'radio'" :key="item.id">
<header>
<span>{{ item.label }}</span>
</header>
<div>
<MkRadio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @update:modelValue="onChangeForm(item)">{{ r.label }}</MkRadio>
</div>
</div>
<div class="card" v-if="item.type == 'slider'" :key="item.id">
<header>
<span>{{ item.label }}</span>
</header>
<div>
<input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/>
</div>
</div>
<div class="card" v-if="item.type == 'textbox'" :key="item.id">
<header>
<span>{{ item.label }}</span>
</header>
<div>
<input v-model="item.value" @change="onChangeForm(item)"/>
</div>
</div>
</template>
</div>
</div>
</div>
<footer class="_acrylic">
<p class="status">
<template v-if="isAccepted && isOpAccepted">{{ $t('_reversi.thisGameIsStartedSoon') }}<MkEllipsis/></template>
<template v-if="isAccepted && !isOpAccepted">{{ $t('_reversi.waitingForOther') }}<MkEllipsis/></template>
<template v-if="!isAccepted && isOpAccepted">{{ $t('_reversi.waitingForMe') }}</template>
<template v-if="!isAccepted && !isOpAccepted">{{ $t('_reversi.waitingBoth') }}<MkEllipsis/></template>
</p>
<div class="actions">
<MkButton inline @click="exit">{{ $t('cancel') }}</MkButton>
<MkButton inline primary @click="accept" v-if="!isAccepted">{{ $t('_reversi.ready') }}</MkButton>
<MkButton inline primary @click="cancel" v-if="isAccepted">{{ $t('_reversi.cancelReady') }}</MkButton>
</div>
</footer>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
import * as maps from '../../../games/reversi/maps';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkRadio from '@/components/ui/radio.vue';
export default defineComponent({
components: {
MkButton,
MkSwitch,
MkRadio,
},
props: {
initGame: {
type: Object,
require: true
},
connection: {
type: Object,
require: true
},
},
data() {
return {
game: this.initGame,
o: null,
isLlotheo: false,
mapName: maps.eighteight.name,
maps: maps,
form: null,
messages: [],
fasCircle, farCircle
};
},
computed: {
mapCategories(): string[] {
const categories = Object.values(maps).map(x => x.category);
return categories.filter((item, pos) => categories.indexOf(item) == pos);
},
isAccepted(): boolean {
if (this.game.user1Id == this.$store.state.i.id && this.game.user1Accepted) return true;
if (this.game.user2Id == this.$store.state.i.id && this.game.user2Accepted) return true;
return false;
},
isOpAccepted(): boolean {
if (this.game.user1Id != this.$store.state.i.id && this.game.user1Accepted) return true;
if (this.game.user2Id != this.$store.state.i.id && this.game.user2Accepted) return true;
return false;
}
},
created() {
this.connection.on('changeAccepts', this.onChangeAccepts);
this.connection.on('updateSettings', this.onUpdateSettings);
this.connection.on('initForm', this.onInitForm);
this.connection.on('message', this.onMessage);
if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1;
if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2;
},
beforeUnmount() {
this.connection.off('changeAccepts', this.onChangeAccepts);
this.connection.off('updateSettings', this.onUpdateSettings);
this.connection.off('initForm', this.onInitForm);
this.connection.off('message', this.onMessage);
},
methods: {
exit() {
},
accept() {
this.connection.send('accept', {});
},
cancel() {
this.connection.send('cancelAccept', {});
},
onChangeAccepts(accepts) {
this.game.user1Accepted = accepts.user1;
this.game.user2Accepted = accepts.user2;
},
updateSettings(key: string) {
this.connection.send('updateSettings', {
key: key,
value: this.game[key]
});
},
onUpdateSettings({ key, value }) {
this.game[key] = value;
if (this.game.map == null) {
this.mapName = null;
} else {
const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
this.mapName = found ? found.name : '-Custom-';
}
},
onInitForm(x) {
if (x.userId == this.$store.state.i.id) return;
this.form = x.form;
},
onMessage(x) {
if (x.userId == this.$store.state.i.id) return;
this.messages.unshift(x.message);
},
onChangeForm(item) {
this.connection.send('updateForm', {
id: item.id,
value: item.value
});
},
onMapChange() {
if (this.mapName == null) {
this.game.map = null;
} else {
this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
}
this.updateSettings('map');
},
onPixelClick(pos, pixel) {
const x = pos % this.game.map[0].length;
const y = Math.floor(pos / this.game.map[0].length);
const newPixel =
pixel == ' ' ? '-' :
pixel == '-' ? 'b' :
pixel == 'b' ? 'w' :
' ';
const line = this.game.map[y].split('');
line[x] = newPixel;
this.game.map[y] = line.join('');
this.updateSettings('map');
}
}
});
</script>
<style lang="scss" scoped>
.urbixznjwwuukfsckrwzwsqzsxornqij {
text-align: center;
background: var(--bg);
> header {
padding: 8px;
border-bottom: dashed 1px #c4cdd4;
}
> div {
padding: 0 16px;
> .card {
margin: 0 auto 16px auto;
&.map {
> header {
> select {
width: 100%;
padding: 12px 14px;
background: var(--face);
border: 1px solid var(--inputBorder);
border-radius: 4px;
color: var(--fg);
cursor: pointer;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
&:focus,
&:active {
border-color: var(--accent);
}
}
}
> div {
> .random {
padding: 32px 0;
font-size: 64px;
color: var(--fg);
opacity: 0.7;
}
> .board {
display: grid;
grid-gap: 4px;
width: 300px;
height: 300px;
margin: 0 auto;
color: var(--fg);
> div {
background: transparent;
border: solid 2px var(--divider);
border-radius: 6px;
overflow: hidden;
cursor: pointer;
* {
pointer-events: none;
user-select: none;
width: 100%;
height: 100%;
}
&.none {
border-color: transparent;
}
}
}
}
}
&.form {
> div {
> .card + .card {
margin-top: 16px;
}
input[type='range'] {
width: 100%;
}
}
}
}
.card {
max-width: 400px;
> header {
padding: 18px 20px;
border-bottom: 1px solid var(--divider);
}
> div {
padding: 20px;
color: var(--fg);
}
}
}
> footer {
position: sticky;
bottom: 0;
padding: 16px;
border-top: solid 1px var(--divider);
> .status {
margin: 0 0 16px 0;
}
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div v-if="game == null"><MkLoading/></div>
<GameSetting v-else-if="!game.isStarted" :init-game="game" :connection="connection"/>
<GameBoard v-else :init-game="game" :connection="connection"/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import GameSetting from './game.setting.vue';
import GameBoard from './game.board.vue';
import * as os from '@/os';
import { faGamepad } from '@fortawesome/free-solid-svg-icons';
export default defineComponent({
components: {
GameSetting,
GameBoard,
},
props: {
gameId: {
type: String,
required: true
},
},
data() {
return {
INFO: {
header: [{
title: this.$t('_reversi.reversi'),
icon: faGamepad
}]
},
game: null,
connection: null,
};
},
watch: {
gameId() {
this.fetch();
}
},
mounted() {
this.fetch();
},
beforeUnmount() {
if (this.connection) {
this.connection.dispose();
}
},
methods: {
fetch() {
os.api('games/reversi/games/show', {
gameId: this.gameId
}).then(game => {
this.game = game;
if (this.connection) {
this.connection.dispose();
}
this.connection = os.stream.connectToChannel('gamesReversiGame', {
gameId: this.game.id
});
this.connection.on('started', this.onStarted);
});
},
onStarted(game) {
Object.assign(this.game, game);
},
}
});
</script>

View File

@@ -0,0 +1,281 @@
<template>
<div class="bgvwxkhb" v-if="!matching">
<h1>Misskey {{ $t('_reversi.reversi') }}</h1>
<div class="play">
<MkButton primary round @click="match" style="margin: var(--margin) auto 0 auto;">{{ $t('invite') }}</MkButton>
</div>
<div class="_section">
<MkFolder v-if="invitations.length > 0">
<template #header>{{ $t('invitations') }}</template>
<div class="nfcacttm">
<button class="invitation _panel _button" v-for="invitation in invitations" tabindex="-1" @click="accept(invitation)">
<MkAvatar class="avatar" :user="invitation.parent"/>
<span class="name"><b><MkUserName :user="invitation.parent"/></b></span>
<span class="username">@{{ invitation.parent.username }}</span>
<MkTime :time="invitation.createdAt" class="time"/>
</button>
</div>
</MkFolder>
<MkFolder v-if="myGames.length > 0">
<template #header>{{ $t('_reversi.myGames') }}</template>
<div class="knextgwz">
<MkA class="game _panel" v-for="g in myGames" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id">
<div class="players">
<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
</div>
<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $t('_reversi.ended') : $t('_reversi.playing') }}</span><MkTime class="time" :time="g.createdAt"/></footer>
</MkA>
</div>
</MkFolder>
<MkFolder v-if="games.length > 0">
<template #header>{{ $t('_reversi.allGames') }}</template>
<div class="knextgwz">
<MkA class="game _panel" v-for="g in games" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id">
<div class="players">
<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
</div>
<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $t('_reversi.ended') : $t('_reversi.playing') }}</span><MkTime class="time" :time="g.createdAt"/></footer>
</MkA>
</div>
</MkFolder>
</div>
</div>
<div class="sazhgisb" v-else>
<h1>
<i18n-t keypath="waitingFor" tag="span">
<template #x>
<b><MkUserName :user="matching"/></b>
</template>
</i18n-t>
<MkEllipsis/>
</h1>
<div class="cancel">
<MkButton inline round @click="cancel">{{ $t('cancel') }}</MkButton>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as os from '@/os';
import MkButton from '@/components/ui/button.vue';
import MkFolder from '@/components/ui/folder.vue';
import { faGamepad } from '@fortawesome/free-solid-svg-icons';
export default defineComponent({
components: {
MkButton, MkFolder,
},
inject: ['navHook'],
data() {
return {
INFO: {
header: [{
title: this.$t('_reversi.reversi'),
icon: faGamepad
}]
},
games: [],
gamesFetching: true,
gamesMoreFetching: false,
myGames: [],
matching: null,
invitations: [],
connection: null,
pingClock: null,
};
},
mounted() {
if (this.$store.getters.isSignedIn) {
this.connection = os.stream.useSharedConnection('gamesReversi');
this.connection.on('invited', this.onInvited);
this.connection.on('matched', this.onMatched);
this.pingClock = setInterval(() => {
if (this.matching) {
this.connection.send('ping', {
id: this.matching.id
});
}
}, 3000);
os.api('games/reversi/games', {
my: true
}).then(games => {
this.myGames = games;
});
os.api('games/reversi/invitations').then(invitations => {
this.invitations = this.invitations.concat(invitations);
});
}
os.api('games/reversi/games').then(games => {
this.games = games;
this.gamesFetching = false;
});
},
beforeUnmount() {
if (this.connection) {
this.connection.dispose();
clearInterval(this.pingClock);
}
},
methods: {
go(game) {
const url = '/games/reversi/' + game.id;
if (this.navHook) {
this.navHook(url);
} else {
this.$router.push(url);
}
},
async match() {
const user = await os.selectUser({ local: true });
if (user == null) return;
os.api('games/reversi/match', {
userId: user.id
}).then(res => {
if (res == null) {
this.matching = user;
} else {
this.go(res);
}
});
},
cancel() {
this.matching = null;
os.api('games/reversi/match/cancel');
},
accept(invitation) {
os.api('games/reversi/match', {
userId: invitation.parent.id
}).then(game => {
if (game) {
this.go(game);
}
});
},
onMatched(game) {
this.go(game);
},
onInvited(invite) {
this.invitations.unshift(invite);
}
}
});
</script>
<style lang="scss" scoped>
.bgvwxkhb {
> h1 {
margin: 0;
padding: 24px;
text-align: center;
font-size: 1.5em;
background: linear-gradient(0deg, #43c583, #438881);
color: #fff;
}
> .play {
text-align: center;
}
}
.sazhgisb {
text-align: center;
}
.nfcacttm {
> .invitation {
display: flex;
box-sizing: border-box;
width: 100%;
padding: 16px;
line-height: 32px;
text-align: left;
> .avatar {
width: 32px;
height: 32px;
margin-right: 8px;
}
> .name {
margin-right: 8px;
}
> .username {
margin-right: 8px;
opacity: 0.7;
}
> .time {
margin-left: auto;
opacity: 0.7;
}
}
}
.knextgwz {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: var(--margin);
> .game {
> .players {
text-align: center;
padding: 16px;
line-height: 32px;
> .avatar {
width: 32px;
height: 32px;
&:first-child {
margin-right: 8px;
}
&:last-child {
margin-left: 8px;
}
}
}
> footer {
display: flex;
align-items: baseline;
border-top: solid 1px var(--divider);
padding: 6px 8px;
font-size: 0.9em;
> .state {
&.playing {
color: var(--accent);
}
}
> .time {
margin-left: auto;
opacity: 0.7;
}
}
}
}
</style>

View File

@@ -1,9 +1,14 @@
<template>
<section class="_section">
<div class="_content">
<MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton>
<div>
<div class="_section">
<div class="_content">
<MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton>
</div>
</div>
</section>
<div class="_section">
<MkA to="/api-console" :behavior="isDesktop ? 'window' : null">API console</MkA>
</div>
</div>
</template>
<script lang="ts">
@@ -28,6 +33,7 @@ export default defineComponent({
icon: faKey
}]
},
isDesktop: window.innerWidth >= 1100,
};
},

View File

@@ -51,6 +51,12 @@
<MkRadio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></MkRadio>
<MkRadio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></MkRadio>
</div>
<div class="_content">
<div>{{ $t('instanceTicker') }}</div>
<MkRadio v-model="instanceTicker" value="none">{{ $t('_instanceTicker.none') }}</MkRadio>
<MkRadio v-model="instanceTicker" value="remote">{{ $t('_instanceTicker.remote') }}</MkRadio>
<MkRadio v-model="instanceTicker" value="always">{{ $t('_instanceTicker.always') }}</MkRadio>
</div>
</section>
<section class="_card _vMargin">
@@ -169,6 +175,11 @@ export default defineComponent({
set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
},
instanceTicker: {
get() { return this.$store.state.device.instanceTicker; },
set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); }
},
enableInfiniteScroll: {
get() { return this.$store.state.device.enableInfiniteScroll; },
set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }

View File

@@ -9,10 +9,9 @@
<option value="mute">{{ $t('_exportOrImport.muteList') }}</option>
<option value="blocking">{{ $t('_exportOrImport.blockingList') }}</option>
</MkSelect>
<MkButton inline @click="doExport()"><Fa :icon="faDownload"/> {{ $t('export') }}</MkButton>
<MkButton inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><Fa :icon="faUpload"/> {{ $t('import') }}</MkButton>
<MkButton inline primary @click="doExport"><Fa :icon="faDownload"/> {{ $t('export') }}</MkButton>
<MkButton inline primary @click="doImport" :disabled="!['following', 'user-lists'].includes(exportTarget)"><Fa :icon="faUpload"/> {{ $t('import') }}</MkButton>
</div>
<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
</section>
</template>
@@ -21,8 +20,8 @@ import { defineComponent } from 'vue';
import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/ui/select.vue';
import { apiUrl } from '@/config';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
export default defineComponent({
components: {
@@ -59,29 +58,9 @@ export default defineComponent({
});
},
doImport() {
(this.$refs.file as any).click();
},
onChangeFile() {
const [file] = Array.from((this.$refs.file as any).files);
async doImport(e) {
const file = await selectFile(e.currentTarget || e.target);
const data = new FormData();
data.append('file', file);
data.append('i', this.$store.state.i.token);
const promise = fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: data
})
.then(response => response.json())
.then(f => {
this.reqImport(f);
});
os.promiseDialog(promise);
},
reqImport(file) {
os.api(
this.exportTarget == 'following' ? 'i/import-following' :
this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
@@ -98,7 +77,7 @@ export default defineComponent({
text: e.message
});
});
}
},
}
});
</script>

View File

@@ -20,6 +20,7 @@
</div>
<div class="menu">
<div class="label">{{ $t('otherSettings') }}</div>
<MkA class="item" :class="{ active: page === 'import-export' }" replace to="/settings/import-export"><Fa :icon="faBoxes" fixed-width class="icon"/>{{ $t('importAndExport') }}</MkA>
<MkA class="item" :class="{ active: page === 'mute-block' }" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</MkA>
<MkA class="item" :class="{ active: page === 'word-mute' }" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</MkA>
<MkA class="item" :class="{ active: page === 'api' }" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</MkA>
@@ -39,7 +40,7 @@
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, onMounted, ref } from 'vue';
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey } from '@fortawesome/free-solid-svg-icons';
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
import { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons';
import { store } from '@/store';
import { i18n } from '@/i18n';
@@ -103,7 +104,7 @@ export default defineComponent({
store.dispatch('logout');
location.href = '/';
},
faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey,
faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes,
};
},
});
@@ -119,9 +120,6 @@ export default defineComponent({
}
.vvcocwet {
max-width: 1000px;
margin: 0 auto;
> .nav {
> .menu {
margin: 16px 0;
@@ -171,7 +169,7 @@ export default defineComponent({
> .nav {
width: 30%;
max-width: 260px;
max-width: 300px;
}
> .main {

View File

@@ -1,5 +1,5 @@
<template>
<section class="_section" v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
<section class="_section">
<div class="_content" v-if="enableTwitterIntegration">
<header><Fa :icon="faTwitter"/> Twitter</header>
<p v-if="integrations.twitter">{{ $t('connectedTo') }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>

View File

@@ -70,6 +70,9 @@ export const router = createRouter({
{ path: '/instance/abuses', component: page('instance/abuses') },
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
{ path: '/games/reversi', component: page('reversi/index') },
{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
{ path: '/api-console', component: page('api-console') },
{ path: '/auth/:token', component: page('auth') },
{ path: '/miauth/:session', component: page('miauth') },
{ path: '/authorize-follow', component: page('follow') },

View File

@@ -115,7 +115,7 @@ export const sidebarDef = {
games: {
title: 'games',
icon: faGamepad,
to: '/games',
to: '/games/reversi',
},
scratchpad: {
title: 'scratchpad',

View File

@@ -77,6 +77,7 @@ export const defaultDeviceSettings = {
enableInfiniteScroll: true,
useBlurEffectForModal: true,
sidebarDisplay: 'full', // full, icon, hide
instanceTicker: 'remote', // none, remote, always
roomGraphicsQuality: 'medium',
roomUseOrthographicCamera: true,
deckColumnAlign: 'left',

View File

@@ -40,8 +40,8 @@ import { } from '@fortawesome/free-regular-svg-icons';
import { v4 as uuid } from 'uuid';
import { host } from '@/config';
import { search } from '@/scripts/search';
import DeckColumnCore from '@/components/deck/column-core.vue';
import DeckColumn from '@/components/deck/column.vue';
import DeckColumnCore from '@/ui/deck/column-core.vue';
import DeckColumn from '@/ui/deck/column.vue';
import XSidebar from '@/components/sidebar.vue';
import XHeader from './_common_/header.vue';
import { getScrollContainer } from '@/scripts/scroll';

View File

@@ -12,7 +12,7 @@
import { defineComponent } from 'vue';
import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons';
import XColumn from './column.vue';
import XTimeline from '../timeline.vue';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
export default defineComponent({

View File

@@ -11,7 +11,7 @@ import { defineComponent } from 'vue';
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
import Progress from '@/scripts/loading';
import XColumn from './column.vue';
import XNotes from '../notes.vue';
import XNotes from '@/components/notes.vue';
import * as os from '@/os';
export default defineComponent({

View File

@@ -12,7 +12,7 @@
import { defineComponent } from 'vue';
import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons';
import XColumn from './column.vue';
import XTimeline from '../timeline.vue';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
export default defineComponent({

View File

@@ -11,7 +11,7 @@ import { defineComponent } from 'vue';
import { faAt } from '@fortawesome/free-solid-svg-icons';
import Progress from '@/scripts/loading';
import XColumn from './column.vue';
import XNotes from '../notes.vue';
import XNotes from '@/components/notes.vue';
import * as os from '@/os';
export default defineComponent({

View File

@@ -11,7 +11,7 @@ import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell } from '@fortawesome/free-regular-svg-icons';
import XColumn from './column.vue';
import XNotifications from '../notifications.vue';
import XNotifications from '@/components/notifications.vue';
import * as os from '@/os';
export default defineComponent({

View File

@@ -23,7 +23,7 @@
import { defineComponent } from 'vue';
import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons';
import XColumn from './column.vue';
import XTimeline from '../timeline.vue';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
export default defineComponent({

View File

@@ -3,7 +3,7 @@
<XSidebar ref="nav" class="sidebar"/>
<div class="contents" ref="contents" :class="{ wallpaper }">
<header class="header" ref="header">
<header class="header" ref="header" @contextmenu.prevent.stop="onContextmenu">
<XHeader :info="pageInfo"/>
</header>
<main ref="main">
@@ -55,7 +55,7 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
import { faLayerGroup, faBars, faHome, faCircle } from '@fortawesome/free-solid-svg-icons';
import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faBell } from '@fortawesome/free-regular-svg-icons';
import { host } from '@/config';
import { search } from '@/scripts/search';
@@ -80,9 +80,9 @@ export default defineComponent({
provide() {
return {
sideViewHook: (url) => {
sideViewHook: this.isDesktop ? (url) => {
this.$refs.side.navigate(url);
}
} : null
};
},
@@ -215,6 +215,26 @@ export default defineComponent({
if (window._scroll) window._scroll();
},
onContextmenu(e) {
const url = this.$route.path;
os.contextMenu([{
type: 'label',
text: url,
}, {
icon: faColumns,
text: this.$t('openInSideView'),
action: () => {
this.$refs.side.navigate(url);
}
}, {
icon: faWindowMaximize,
text: this.$t('openInWindow'),
action: () => {
os.pageWindow(url);
}
}], e);
},
async onNotification(notification) {
if (this.$store.state.i.mutingNotificationTypes.includes(notification.type)) {
return;

View File

@@ -4,11 +4,11 @@
<MkA class="link" to="/">{{ $t('home') }}</MkA>
<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
<MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName || host }) }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
</header>
<div class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1>{{ instanceName || host }}</h1>
<h1>{{ instanceName }}</h1>
</div>
<div class="contents" ref="contents" :class="{ wallpaper }">

View File

@@ -77,7 +77,7 @@ export default defineComponent({
> a {
display: block;
padding: 8px 16px;
color: var(--text);
color: var(--fg);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;

View File

@@ -279,7 +279,6 @@
{ "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF7C", "name": "person_feeding_baby", "keywords": [] },
{ "category": "people", "char": "👸", "name": "princess", "keywords": ["girl", "woman", "female", "blond", "crown", "royal", "queen"] },
{ "category": "people", "char": "🤴", "name": "prince", "keywords": ["boy", "man", "male", "crown", "royal", "king"] },
{ "category": "people", "char": "🤴", "name": "prince", "keywords": ["boy", "man", "male", "crown", "royal", "king"] },
{ "category": "people", "char": "👰", "name": "person_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] },
{ "category": "people", "char": "👰", "name": "bride_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] },
{ "category": "people", "char": "🤵", "name": "person_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] },
@@ -517,6 +516,7 @@
{ "category": "animals_and_nature", "char": "🦚", "name": "peacock", "keywords": ["animal", "nature", "peahen", "bird"] },
{ "category": "animals_and_nature", "char": "🦜", "name": "parrot", "keywords": ["animal", "nature", "bird", "pirate", "talk"] },
{ "category": "animals_and_nature", "char": "🦞", "name": "lobster", "keywords": ["animal", "nature", "bisque", "claws", "seafood"] },
{ "category": "animals_and_nature", "char": "🦠", "name": "microbe", "keywords": ["amoeba", "bacteria", "germs"] },
{ "category": "animals_and_nature", "char": "🦟", "name": "mosquito", "keywords": ["animal", "nature", "insect", "malaria"] },
{ "category": "animals_and_nature", "char": "🦬", "name": "bison", "keywords": ["animal", "nature"] },
{ "category": "animals_and_nature", "char": "🦣", "name": "mammoth", "keywords": ["animal", "nature"] },
@@ -870,7 +870,7 @@
{ "category": "travel_and_places", "char": "🚡", "name": "aerial_tramway", "keywords": ["transportation", "vehicle", "ski"] },
{ "category": "travel_and_places", "char": "🚠", "name": "mountain_cableway", "keywords": ["transportation", "vehicle", "ski"] },
{ "category": "travel_and_places", "char": "🚟", "name": "suspension_railway", "keywords": ["vehicle", "transportation"] },
{ "category": "travel_and_places", "char": "🚃", "name": "railway_car", "keywords": ["transportation", "vehicle"] },
{ "category": "travel_and_places", "char": "🚃", "name": "railway_car", "keywords": ["transportation", "vehicle", "train"] },
{ "category": "travel_and_places", "char": "🚋", "name": "train", "keywords": ["transportation", "vehicle", "carriage", "public", "travel"] },
{ "category": "travel_and_places", "char": "🚝", "name": "monorail", "keywords": ["transportation", "vehicle"] },
{ "category": "travel_and_places", "char": "🚄", "name": "bullettrain_side", "keywords": ["transportation", "vehicle"] },
@@ -1075,7 +1075,6 @@
{ "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
{ "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] },
{ "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] },
{ "category": "objects", "char": "🦠", "name": "microbe", "keywords": ["amoeba", "bacteria", "germs"] },
{ "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] },
{ "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] },
{ "category": "objects", "char": "🌡", "name": "thermometer", "keywords": ["weather", "temperature", "hot", "cold"] },

View File

@@ -142,51 +142,6 @@ export const eighteightH4: Map = {
]
};
export const eighteightH12: Map = {
name: '8x8 handicap 12',
category: '8x8',
data: [
'bb----bb',
'b------b',
'--------',
'---wb---',
'---bw---',
'--------',
'b------b',
'bb----bb'
]
};
export const eighteightH16: Map = {
name: '8x8 handicap 16',
category: '8x8',
data: [
'bbb---bb',
'b------b',
'-------b',
'---wb---',
'---bw---',
'b-------',
'b------b',
'bb---bbb'
]
};
export const eighteightH20: Map = {
name: '8x8 handicap 20',
category: '8x8',
data: [
'bbb--bbb',
'b------b',
'b------b',
'---wb---',
'---bw---',
'b------b',
'b------b',
'bbb---bb'
]
};
export const eighteightH28: Map = {
name: '8x8 handicap 28',
category: '8x8',

View File

@@ -163,6 +163,16 @@ export class Instance {
})
public iconUrl: string | null;
@Column('varchar', {
length: 256, nullable: true, default: null,
})
public faviconUrl: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public themeColor: string | null;
@Column('timestamp with time zone', {
nullable: true,
})

View File

@@ -1,7 +1,7 @@
import $ from 'cafy';
import { EntityRepository, Repository, In, Not } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings } from '..';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..';
import { ensure } from '../../prelude/ensure';
import config from '../../config';
import { SchemaType } from '../../misc/schema';
@@ -181,6 +181,14 @@ export class UserRepository extends Repository<User> {
isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy,
isCat: user.isCat || falsy,
instance: user.host ? Instances.findOne({ host: user.host }).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
} : undefined) : undefined,
// カスタム絵文字添付
emojis: user.emojis.length > 0 ? Emojis.find({

View File

@@ -0,0 +1,28 @@
import $ from 'cafy';
import define from '../../../define';
import { Instances } from '../../../../../models';
import { toPuny } from '../../../../../misc/convert-host';
import { fetchInstanceMetadata } from '../../../../../services/fetch-instance-metadata';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
host: {
validator: $.str
},
}
};
export default define(meta, async (ps, me) => {
const instance = await Instances.findOne({ host: toPuny(ps.host) });
if (instance == null) {
throw new Error('instance not found');
}
fetchInstanceMetadata(instance, true);
});

View File

@@ -243,20 +243,23 @@ export default class extends Channel {
if (game.isEnded) return;
if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
const myColor =
((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
? true
: false;
const o = new Reversi(game.map, {
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard
});
// 盤面の状態を再生
for (const log of game.logs) {
o.put(log.color, log.pos);
}
const myColor =
((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
? true
: false;
if (o.turn !== myColor) return;
if (!o.canPut(myColor, pos)) return;
o.put(myColor, pos);

View File

@@ -11,6 +11,7 @@ html
meta(name='application-name' content='Misskey')
meta(name='referrer' content='origin')
meta(name='theme-color' content='#86b300')
meta(name='theme-color-orig' content='#86b300')
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='icon' href= icon || '/favicon.ico')

View File

@@ -1,4 +1,4 @@
import { JSDOM } from 'jsdom';
import { DOMWindow, JSDOM } from 'jsdom';
import fetch from 'node-fetch';
import { getJson, getHtml, getAgentByUrl } from '../misc/fetch';
import { Instance } from '../models/entities/instance';
@@ -9,22 +9,33 @@ import { URL } from 'url';
const logger = new Logger('metadata', 'cyan');
export async function fetchInstanceMetadata(instance: Instance): Promise<void> {
export async function fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
const unlock = await getFetchInstanceMetadataLock(instance.host);
const _instance = await Instances.findOne({ host: instance.host });
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
unlock();
return;
if (!force) {
const _instance = await Instances.findOne({ host: instance.host });
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
unlock();
return;
}
}
logger.info(`Fetching metadata of ${instance.host} ...`);
try {
const [info, icon] = await Promise.all([
const [info, dom, manifest] = await Promise.all([
fetchNodeinfo(instance).catch(() => null),
fetchIconUrl(instance).catch(() => null),
fetchDom(instance).catch(() => null),
fetchManifest(instance).catch(() => null),
]);
const [favicon, icon, themeColor, name, description] = await Promise.all([
fetchFaviconUrl(instance, dom).catch(() => null),
fetchIconUrl(instance, dom, manifest).catch(() => null),
getThemeColor(dom, manifest).catch(() => null),
getSiteName(info, dom, manifest).catch(() => null),
getDescription(info, dom, manifest).catch(() => null),
]);
logger.succ(`Successfuly fetched metadata of ${instance.host}`);
@@ -34,18 +45,18 @@ export async function fetchInstanceMetadata(instance: Instance): Promise<void> {
} as Record<string, any>;
if (info) {
updates.softwareName = info.software.name.toLowerCase();
updates.softwareVersion = info.software.version;
updates.softwareName = info.software?.name.toLowerCase();
updates.softwareVersion = info.software?.version;
updates.openRegistrations = info.openRegistrations;
updates.name = info.metadata ? (info.metadata.nodeName || info.metadata.name || null) : null;
updates.description = info.metadata ? (info.metadata.nodeDescription || info.metadata.description || null) : null;
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null;
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null;
}
if (icon) {
updates.iconUrl = icon;
}
if (name) updates.name = name;
if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = icon || favicon;
if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor;
await Instances.update(instance.id, updates);
@@ -57,7 +68,25 @@ export async function fetchInstanceMetadata(instance: Instance): Promise<void> {
}
}
async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> {
type NodeInfo = {
openRegistrations?: any;
software?: {
name?: any;
version?: any;
};
metadata?: {
name?: any;
nodeName?: any;
nodeDescription?: any;
description?: any;
maintainer?: {
name?: any;
email?: any;
};
};
};
async function fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try {
@@ -100,8 +129,8 @@ async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> {
}
}
async function fetchIconUrl(instance: Instance): Promise<string | null> {
logger.info(`Fetching icon URL of ${instance.host} ...`);
async function fetchDom(instance: Instance): Promise<DOMWindow['document']> {
logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host;
@@ -110,14 +139,28 @@ async function fetchIconUrl(instance: Instance): Promise<string | null> {
const { window } = new JSDOM(html);
const doc = window.document;
const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href');
const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href');
const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
return doc;
}
const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon;
async function fetchManifest(instance: Instance): Promise<Record<string, any> | null> {
const url = 'https://' + instance.host;
if (href) {
return (new URL(href, url)).href;
const manifestUrl = url + '/manifest.json';
const manifest = await getJson(manifestUrl);
return manifest;
}
async function fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
const url = 'https://' + instance.host;
if (doc) {
const href = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
if (href) {
return (new URL(href, url)).href;
}
}
const faviconUrl = url + '/favicon.ico';
@@ -133,3 +176,90 @@ async function fetchIconUrl(instance: Instance): Promise<string | null> {
return null;
}
async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
const url = 'https://' + instance.host;
return (new URL(manifest.icons[0].src, url)).href;
}
if (doc) {
const url = 'https://' + instance.host;
const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href');
const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href');
const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon;
if (href) {
return (new URL(href, url)).href;
}
}
return null;
}
async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (doc) {
const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
if (themeColor) {
return themeColor;
}
}
if (manifest) {
return manifest.theme_color;
}
return null;
}
async function getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (info && info.metadata) {
if (info.metadata.nodeName || info.metadata.name) {
return info.metadata.nodeName || info.metadata.name;
}
}
if (doc) {
const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
if (og) {
return og;
}
}
if (manifest) {
return manifest?.name || manifest?.short_name;
}
return null;
}
async function getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (info && info.metadata) {
if (info.metadata.nodeDescription || info.metadata.description) {
return info.metadata.nodeDescription || info.metadata.description;
}
}
if (doc) {
const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
if (meta) {
return meta;
}
const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
if (og) {
return og;
}
}
if (manifest) {
return manifest?.name || manifest?.short_name;
}
return null;
}