Compare commits

...

33 Commits

Author SHA1 Message Date
a4b7a9db03 12.42.0 2020-07-19 15:30:31 +09:00
280eeb9d75 fix(client): ✌️ 2020-07-19 12:26:05 +09:00
3f71b14637 feat: Blurhash integration
Resolve #6559
2020-07-19 00:24:07 +09:00
705d40ab37 fix(client): プラグインの動作を修正 2020-07-18 20:03:46 +09:00
b39850de01 feat(client): AiScriptプラグインからAPIアクセスできるように 2020-07-18 14:28:32 +09:00
b9c5e95b85 fix(docs): Update api doc 2020-07-18 12:23:57 +09:00
0c1de7b1b6 feat: トークン手動発行機能 2020-07-18 12:12:10 +09:00
0a4499fd03 Ignore Activities from deleted actors on both ends Fix #6553 (#6554) 2020-07-17 22:47:22 +09:00
b663a47331 feat(client): 設定画面を整理 2020-07-17 22:30:41 +09:00
eb275a62a6 fix(client): Better wheel handling 2020-07-17 21:56:30 +09:00
e18caa3396 feat(client): Deckでマウスホイールを使って横スクロールできるように 2020-07-17 21:53:34 +09:00
eb15d31ebf 12.41.3 2020-07-15 18:27:57 +09:00
e7f1ab2d01 fix(client): Fix #6526 2020-07-15 18:22:19 +09:00
9d3beb3174 fix(client): Fix #6540 2020-07-15 18:03:08 +09:00
b6c3399abe chore: Add note 2020-07-15 00:21:14 +09:00
0a28573845 chore: Add note 2020-07-13 23:29:30 +09:00
937df577f1 fix(client): Fix sticky sidebar behavior 2020-07-13 15:13:02 +09:00
e54fd6c2cb Update CHANGELOG.md 2020-07-13 15:06:51 +09:00
3caea9d33e Update CHANGELOG.md 2020-07-13 15:05:47 +09:00
b9e9631195 feat(client): Add sounds 🎵 2020-07-12 22:17:13 +09:00
76bded455a 12.41.2 2020-07-12 18:37:24 +09:00
c94d9210ed New Crowdin updates (#6527)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Korean)

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

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

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

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)
2020-07-12 18:36:42 +09:00
0f5db9558c fix(client): Show shadow 2020-07-12 18:36:14 +09:00
35a8c37922 fix(client): Fix theme color 2020-07-12 18:22:13 +09:00
6ff84a1061 refactor 2020-07-12 18:19:02 +09:00
aae9bc4cf4 feat(client): blur effect for modal 2020-07-12 18:14:59 +09:00
426c2fa5d1 fix(locale): Add missing key 2020-07-12 17:44:27 +09:00
dab728278c fix(client): Fix indicator position 2020-07-12 17:43:35 +09:00
7555ab097a fix(client): i18n 2020-07-12 17:37:56 +09:00
5b5b64d251 fix(client): Fix #6532 2020-07-12 16:14:49 +09:00
eb84445796 fix(client): Fix style 2020-07-12 16:05:00 +09:00
364bd9ae74 fix(server): Fix #6533 2020-07-12 15:35:11 +09:00
d4b0761549 Deckのタイムラインカラムの初回種別選択でキャンセルが押されたらタイムラインカラムを消すように (#6535)
Fix #6531
2020-07-12 15:33:04 +09:00
68 changed files with 910 additions and 414 deletions

View File

@ -1,6 +1,63 @@
ChangeLog
=========
Next (2020/7/)
-------------------
### ✨Improvements
- サウンドを追加 [b9e9631](https://github.com/syuilo/misskey/commit/b9e9631195a8ca5ed1386daeacdc835456d52975)
### 🐛Fixes
-
12.41.2 (2020/7/12)
-------------------
### ✨Improvements
- モーダルにぼかし効果を使用するオプション [aae9bc4c](https://github.com/syuilo/misskey/commit/aae9bc4cf4c583b4d675391fe3da2fa53b7f18e0)
- スタイルの調整 [eb84445](https://github.com/syuilo/misskey/commit/eb84445796039b93d124fa615e96c08fedcd9bf9), [dab7282](https://github.com/syuilo/misskey/commit/dab728278ca577622c575d1968eb6a22c7b444b9), [35a8c379](https://github.com/syuilo/misskey/commit/35a8c37922193317b3f6397562c762f9a9169b91)
### 🐛Fixes
- Deckのタイムラインを追加した直後のタイムライン種別の選択がキャンセルできない問題を修正 [#6535](https://github.com/syuilo/misskey/pull/6535)
- ノート詳細 /notes/:id ページの直リンを踏むと Not Found になる問題を修正 [364bd9a](https://github.com/syuilo/misskey/commit/364bd9ae74226c46ccdad810884bce11b2bef156)
- Deckでメインカラムの「投稿があります」をクリックしても上に行かない問題を修正 [5b5b64d](https://github.com/syuilo/misskey/commit/5b5b64d2514cf445aa81a6750ac4185f4e7dd8cd)
- 翻訳の修正 [7555ab0](https://github.com/syuilo/misskey/commit/7555ab097a6aab68851782b641a33fb3fdf2f101), [426c2fa](https://github.com/syuilo/misskey/commit/426c2fa5d152610516337cc5a53810e136d573db)
12.41.1 (2020/7/12)
-------------------
### ✨Improvements
- ResizeObserver Polyfillを削除 [c89abda](https://github.com/syuilo/misskey/commit/c89abda3fb55857bb81c4f2163a4a0396a04fc27)
* Misskey Webのパフォーマンスが劇的に改善されました
- スタイルの調整 [7cbe95a](https://github.com/syuilo/misskey/commit/7cbe95a1cf67f2536a6332bbccc7129afcd92f73), [320352b](https://github.com/syuilo/misskey/commit/320352bf4ba56ddd67c9c6bc0816dab94c53191b)
### 🐛Fixes
- サイドバーのホームを押すことでのトップへのスクロールが動作しなくなっている問題を修正 [3c66990](https://github.com/syuilo/misskey/commit/3c669902632570bb1354f6b53253037f183718b5)
12.41.0 (2020/7/12)
-------------------
### ✨Improvements
- デッキの実装 [#6504](https://github.com/syuilo/misskey/pull/6504), [065ec8e](https://github.com/syuilo/misskey/commit/065ec8e17080887814b1912233d38e412b2811d2), [debc008](https://github.com/syuilo/misskey/commit/debc0086fab6c131cf37f00e8b03fbe5d6f09c64)
- テーマエディターの実装 [#6482](https://github.com/syuilo/misskey/pull/6482)
- プラグインシステムの実装 [#6479](https://github.com/syuilo/misskey/pull/6479)
- ウィジェットの位置を固定するオプションを追加 [3799708](https://github.com/syuilo/misskey/commit/3799708daf52c221c03ff0b1c11d8b888b22d32f)
- ウィジェットの位置を固定しない場合、Twitterのようにstickyに画面追従するように [c25cf7f](https://github.com/syuilo/misskey/commit/c25cf7f89a1d3d7e55331396bbc3f44920a38de5)
- サウンドを追加 (syuilo/pirori) [d4b4b61](https://github.com/syuilo/misskey/commit/d4b4b61535ee4f5f759ba3342b55e978e43f1c7b)
- タイムライン上でTwitterの埋め込みプレビューを表示できるように [#6496](https://github.com/syuilo/misskey/pull/6496)
- デザインや挙動の調整 [#6495](https://github.com/syuilo/misskey/pull/6495), [752669b](https://github.com/syuilo/misskey/commit/752669bf5ea83b81ddcabb804e795a24debe6dc0), [#6497](https://github.com/syuilo/misskey/pull/6497), [ade11aa](https://github.com/syuilo/misskey/commit/ade11aa447f0102c9202955e01c59fcb501f794e), [27a17b4](https://github.com/syuilo/misskey/commit/27a17b467d72aea81774c04b8ca3e01ed6874b24), [4fd0636](https://github.com/syuilo/misskey/commit/4fd06369d355f032b5eb245dfd98faadee2289f9), [ca2e53b](https://github.com/syuilo/misskey/commit/ca2e53bd6e3de50f2fdf62da16734873be37fcc4), [8ff2694](https://github.com/syuilo/misskey/commit/8ff2694cadd3ab3d51f96fc2ea3bbfde29475660), [11f8d74](https://github.com/syuilo/misskey/commit/11f8d742eb53e8b815abc8ed1c34627dcbaa9e2f)
- ソースコードのリファクタ [a591a33](https://github.com/syuilo/misskey/commit/a591a334ed6fd7f8ed936bf7e7edfcce08de035a)
### 🐛Fixes
- 依存パッケージの更新 [#6491](https://github.com/syuilo/misskey/pull/6491), [#6516](https://github.com/syuilo/misskey/pull/6516), [d327bb8](https://github.com/syuilo/misskey/commit/d327bb8ff1b8765e92d6815d244e74f0793f6157)
- サーバーへのファイルダウンロードのタイムアウトを11秒から60秒に緩和 [#6503](https://github.com/syuilo/misskey/pull/6503)
- 非ログイン時にキーボードショートカットで投稿フォームが開けてしまう問題を修正 [#6508](https://github.com/syuilo/misskey/pull/6508)
- キャッシュされてないリモートファイルのURLが相対URLで返ってくる問題を修正 [#6514](https://github.com/syuilo/misskey/pull/6514)
* リモートファイルをキャッシュしない設定のインスタンスにおいてサードパーティークライアントでリモートの画像が表示できない問題が修正されます
- Mastodon v2.5.0未満からのActivityが受け取れない問題の修正 [#6518](https://github.com/syuilo/misskey/pull/6518)
- music.youtube.comのURLプレビューの修正 [#6496](https://github.com/syuilo/misskey/pull/6496)
- URLプレビューの翻訳を修正 [#6496](https://github.com/syuilo/misskey/pull/6496)
- ートの表示幅が狭いとTwitterウィジェットがはみ出すのをなんとか修正 [#6496](https://github.com/syuilo/misskey/pull/6496)
- HiDPi環境でMisskey v12 Roomの家具を選択できない問題を修正 [#6507](https://github.com/syuilo/misskey/pull/6507)
- Safariでの検索インプット・検索ボタンのデザインが適用されないのを修正 [#6484](https://github.com/syuilo/misskey/pull/6484)
- フォロワーではないリモートユーザーに削除通知が配信されない問題を修正 [#6475](https://github.com/syuilo/misskey/pull/6475)
12.40.0 (2020/7/5)
-------------------
### ✨Improvements

View File

@ -33,6 +33,7 @@ copyLink: "انسخ الرابط"
delete: "حذف"
deleteAndEdit: "إزالة وإعادة الصياغة"
addToList: "أضفه إلى قائمة"
sendMessage: "أرسل رسالة"
copyUsername: "انسخ اسم المستخدم"
reply: "رد"
loadMore: "عرض المزيد"
@ -57,17 +58,20 @@ retry: "حاول مجددًا"
enterListName: "اسم القائمة"
privacy: "الخصوصية"
makeFollowManuallyApprove: "القبول يدويا طلبات الإشتراك"
defaultNoteVisibility: "مدى الرؤية الافتراضي"
follow: "تابِع"
followRequest: "طلب اشتراك"
followRequests: "طلبات الإشتراك"
unfollow: "إلغاء الاشتراك"
followRequestPending: "طلبات الإشتراك المعلّقة"
unrenote: "إلغاء مشاركة الملاحظة"
quote: "اقتبس"
pinnedNote: "ملاحظة مدبسة"
you: "أنت"
clickToShow: "اضغط للعرض"
sensitive: "محتوى حساس"
add: "إضافة"
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"
enterFileName: "ادخل اسم الملف"
mute: "اكتم"
unmute: "إلغاء الكتم"
@ -227,6 +231,7 @@ manageAntennas: "إدارة الهوائيات"
name: "الإسم"
antennaSource: "مصدر الهوائي"
antennaKeywords: "الكلمات المفتاحية للإستقبال"
withReplies: "بالردود"
notesAndReplies: "الملاحظات والردود"
withFiles: "بالمرفقات"
silence: "اكتم"
@ -250,6 +255,7 @@ unregister: "إلغاء التسجيل"
passwordLessLogin: "لِج مِن دون كلمة سرية"
resetPassword: "أعد تعيين كلمتك السرية"
newPasswordIs: "كلمتك السرية الجديدة هي {password}"
autoNoteWatch: "راقب الملاحظات تلقائيا"
share: "شارِك"
notFound: "غير موجود"
help: "المساعدة"
@ -271,6 +277,8 @@ next: "التالية"
retype: "أعد الكتابة"
noteOf: "ملاحظات {user}"
inviteToGroup: "دعوة إلى فريق"
noMessagesYet: "ليس هناك رسائل بعد"
newMessageExists: "لقد تلقيت رسالة جديدة"
invitationCode: "رمز الدعوة"
checking: "التحقق جارٍ"
available: "متوفر"
@ -288,6 +296,7 @@ uiLanguage: "لغة واجهة المستخدم"
aboutX: "عن {x}"
useOsNativeEmojis: "استخدم الإيموجيات الخاصة بنظام التشغيل"
youHaveNoGroups: "لا تمتلك أية فِرَق"
noHistory: "السجل فارغ"
doing: "انتظر لحظة"
category: "الفئات"
tags: "الوسوم"
@ -330,10 +339,15 @@ rooms: "الغرفة"
relays: "المُرَحلات"
addRelay: "إضافة مُرحّل"
addedRelays: "المرحلات التي تم إضافتها"
deletedNote: "ملاحظة محذوفة"
invisibleNote: "ملاحظة مخفية"
_theme:
explore: "استكشف قوالب المظهر"
keys:
messageBg: "خلفية الدردشة"
_sfx:
note: "الملاحظات"
noteMy: "ملاحظتي"
notification: "الإشعارات"
chat: "الدردشة"
_ago:
@ -409,6 +423,7 @@ _profile:
username: "اسم المستخدم"
youCanIncludeHashtags: "يمكنك أيضًا إضافة وسوم إلى نبذتك التعريفية."
_exportOrImport:
allNotes: "كل الملاحظات"
followingList: "المتابَعون"
muteList: "اكتم"
blockingList: "احجب"
@ -426,6 +441,7 @@ _rooms:
default: "افتراضي"
_furnitures:
monitor: "شاشة التحكم"
banknote: "أوراق نقدية"
_pages:
blocks:
image: "الصور"
@ -453,6 +469,8 @@ _pages:
types:
array: "القوائم"
_notification:
youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
youWereFollowed: "يتابعك"
_deck:
_columns:

View File

@ -523,6 +523,9 @@ themeEditor: "Farbthemen-Editor"
description: "Beschreibung"
author: "Autor"
leaveConfirm: "Es gibt unspeicherte Änderungen. Möchtest du diese verwerfen?"
manage: "Verwaltung"
plugins: "Plugins"
pluginInstallWarn: "Installiere nur vertrauenswürdige Plugins."
deck: "Deck"
undeck: "Deck verlassen"
_theme:
@ -1166,6 +1169,7 @@ _notification:
_deck:
alwaysShowMainColumn: "Hauptspalte immer zeigen"
columnAlign: "Spalten ausrichten"
addColumn: "Spalte hinzufügen"
_columns:
widgets: "Widgets"
notifications: "Benachrichtigungen"

View File

@ -523,6 +523,9 @@ themeEditor: "Theme editor"
description: "Description"
author: "Author"
leaveConfirm: "There are unsaved changes. Do you want to discard them?"
manage: "Management"
plugins: "Plugins"
pluginInstallWarn: "Please do not install untrustworthy plugins."
deck: "Deck"
undeck: "Leave Deck"
_theme:
@ -1166,6 +1169,7 @@ _notification:
_deck:
alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns"
addColumn: "Add column"
_columns:
widgets: "Widgets"
notifications: "Notifications"

View File

@ -523,6 +523,9 @@ themeEditor: "Editor de temas"
description: "Descripción"
author: "Autor"
leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?"
manage: "Administrar"
plugins: "Plugins"
pluginInstallWarn: "Por favor no instale plugins que no son de confianza"
_theme:
explore: "Explorar temas"
install: "Instalar tema"
@ -573,6 +576,27 @@ _theme:
divider: "Divisor"
scrollbarHandle: "Cuadro de la barra de desplazamiento"
scrollbarHandleHover: "Cuadro de la barra de desplazamiento (hover)"
dateLabelFg: "Texto de la etiqueta de fecha"
infoBg: "Fondo de información"
infoFg: "Texto de información"
infoWarnBg: "Fondo de advertencias"
infoWarnFg: "Texto de advertencias"
cwBg: "Fondo del botón CW"
cwFg: "Texto del botón CW"
cwHoverBg: "Fondo del botón CW (hover)"
toastBg: "Fondo de notificaciones"
toastFg: "Texto de notificaciones"
buttonBg: "Fondo de botón"
buttonHoverBg: "Fondo de botón (hover)"
inputBorder: "Borde de los campos de entrada"
listItemHoverBg: "Fondo de elemento de listas (hover)"
driveFolderBg: "Fondo de capeta del drive"
wallpaperOverlay: "Transparencia del fondo de pantalla"
badge: "Medalla"
messageBg: "Fondo de chat"
accentDarken: "Acento (oscuro)"
accentLighten: "Acento (claro)"
fgHighlighted: "Texto resaltado"
_sfx:
note: "Notas"
noteMy: "Nota (a mí mismo)"
@ -686,6 +710,7 @@ _widgets:
rss: "Lector RSS"
activity: "Actividad"
photos: "Fotos"
digitalClock: "Reloj digital"
_cw:
hide: "Ocultar"
show: "Ver más"
@ -1140,7 +1165,10 @@ _notification:
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
youWereInvitedToGroup: "Invitado al grupo"
_deck:
alwaysShowMainColumn: "Siempre mostrar la columna principal"
columnAlign: "Alinear columnas"
_columns:
widgets: "Widgets"
notifications: "Notificaciones"
tl: "Linea de tiempo"
antenna: "Antenas"

View File

@ -516,6 +516,14 @@ visibility: "Visibilité"
poll: "Sondage"
useCw: "Masquer le contenu"
fixedWidgetsPosition: "Rendre la position du widget fixe"
enablePlayer: "Activer le lecteur vidéo"
disablePlayer: "Désactiver le lecteur vidéo"
expandTweet: "Étendre le tweet"
themeEditor: "Éditeur de thèmes"
description: "Description"
author: "Auteur·rice"
manage: "Gestion"
plugins: "Extensions"
_theme:
explore: "Explorer les thèmes"
install: "Installer un thème"
@ -524,12 +532,28 @@ _theme:
installed: "{name} a été installé"
alreadyInstalled: "Ce thème est déjà installé"
invalid: "Le format du thème n'est pas valide"
make: "Créer un thème"
base: "Base"
defaultValue: "Valeur par défaut"
color: "Couleur"
func: "Fonction"
argument: "Argument"
alpha: "Transparence"
darken: "Assombrir"
keys:
bg: "Arrière-plan"
fg: "Texte"
focus: "Mise au point"
indicator: "Indicateur"
panel: "Panneau"
shadow: "Ombre"
header: "Entête"
navBg: "Fond de la barre latérale"
hashtag: "Hashtags"
mention: "Mentionner"
renote: "Renote"
divider: "Séparateur"
messageBg: "Arrière plan de la discussion"
_sfx:
note: "Nouvelle note"
noteMy: "Ma note"
@ -891,7 +915,7 @@ _pages:
pushEvent: "Envoyer un évènement"
_pushEvent:
event: "Nom de lévènement"
message: "Message à afficher lorsque appuyé"
message: "Message à afficher lorsquil est activé"
variable: "Variable à envoyer"
no-variable: "Rien"
callAiScript: "Appeler AiScript"
@ -1097,7 +1121,10 @@ _notification:
yourFollowRequestAccepted: "Votre demande dabonnement a été accepté"
youWereInvitedToGroup: "Invité au groupe"
_deck:
alwaysShowMainColumn: "Toujours afficher la colonne principale"
columnAlign: "Aligner les colonnes"
_columns:
widgets: "Widgets"
notifications: "Notifications"
tl: "Fil"
antenna: "Antennes"

View File

@ -442,7 +442,7 @@ remote: "リモート"
total: "合計"
weekOverWeekChanges: "前週比"
dayOverDayChanges: "前日比"
accessibility: "アクセシビリティ"
appearance: "アピアランス"
clinetSettings: "クライアント設定"
accountSettings: "アカウント設定"
promotion: "プロモーション"
@ -528,6 +528,13 @@ plugins: "プラグイン"
pluginInstallWarn: "信頼できないプラグインはインストールしないでください。"
deck: "デッキ"
undeck: "デッキ解除"
useBlurEffectForModal: "モーダルにぼかし効果を使用"
generateAccessToken: "アクセストークンの発行"
permission: "権限"
enableAll: "全て有効にする"
disableAll: "全て無効にする"
tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
_theme:
explore: "テーマを探す"
@ -1206,6 +1213,7 @@ _notification:
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
addColumn: "カラムを追加"
_columns:
widgets: "ウィジェット"

View File

@ -511,9 +511,23 @@ addedRelays: "추가된 릴레이"
serviceworkerInfo: "푸시 알림을 수행하려면 활성화해야 합니다."
deletedNote: "삭제된 노트"
invisibleNote: "비공개 노트"
enableInfiniteScroll: "자동으로 좀 더 보기"
visibility: "공개 범위"
poll: "투표"
useCw: "내용 숨기기"
fixedWidgetsPosition: "위젯의 위치 고정"
enablePlayer: "플레이어 열기"
disablePlayer: "플레이어 닫기"
expandTweet: "트윗 확장하기"
themeEditor: "테마 에디터"
description: "설명"
author: "작성자"
leaveConfirm: "저장하지 않은 변경사항이 있습니다. 취소하시겠습니까?"
manage: "관리"
plugins: "플러그인"
pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오."
deck: "덱"
undeck: "덱 해제"
_theme:
explore: "테마 찾아보기"
install: "테마 설치"
@ -522,8 +536,18 @@ _theme:
installed: "{name} 테마가 설치되었습니다"
alreadyInstalled: "이미 설치된 테마입니다"
invalid: "테마 형식이 올바르지 않습니다"
make: "테마 만들기"
base: "베이스"
addConstant: "상수 추가"
constant: "상수"
defaultValue: "기본값"
color: "색"
refProp: "프로퍼티를 참조"
refConst: "상수를 참조"
key: "키"
func: "함수"
funcKind: "함수 종류"
argument: "매개변수"
keys:
mention: "멘션"
renote: "Renote"

View File

@ -46,7 +46,7 @@ youGotNewFollower: "你有新的关注者"
receiveFollowRequest: "您收到了关注请求"
followRequestAccepted: "您的关注请求被通过了"
mention: "提及"
mentions: "提到我的"
mentions: "提"
directNotes: "私信"
importAndExport: "导入和导出"
import: "导入"
@ -523,6 +523,9 @@ themeEditor: "主题编辑器"
description: "描述"
author: "作者"
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
manage: "管理"
plugins: "插件"
pluginInstallWarn: "请不要安装不明来源的插件"
deck: "Deck"
undeck: "取消Deck"
_theme:
@ -534,17 +537,66 @@ _theme:
alreadyInstalled: "此主题已经安装"
invalid: "主题格式错误"
make: "主题制作"
base: "基于"
addConstant: "添加常量"
constant: "常量"
defaultValue: "默认值"
color: "颜色"
refProp: "查看属性"
refConst: "查看常量"
key: "主要"
func: "函数"
funcKind: "功能类型"
argument: "参数"
basedProp: "基于的属性名称"
alpha: "不透明度"
darken: "暗色"
lighten: "亮色"
inputConstantName: "请输入常量名称"
importInfo: "您可以在此处粘贴主题代码,将其导入到编辑器中"
deleteConstantConfirm: "确定要删除常量{const}吗?"
keys:
header: "页眉"
accent: "强调色"
bg: "背景"
fg: "文本"
focus: "聚焦"
indicator: "标记"
panel: "面板"
shadow: "阴影"
header: "顶栏"
navBg: "侧边栏背景"
navFg: "侧栏文本"
navHoverFg: "侧栏文本(悬停)"
navActive: "侧栏文本(活动)"
navIndicator: "侧栏标记"
link: "链接"
hashtag: "话题标签"
mention: "提及"
mentionMe: "提及"
renote: "转发"
modalBg: "模块背景"
divider: "分割线"
scrollbarHandle: "滚动条"
scrollbarHandleHover: "滚动条(悬停)"
dateLabelFg: "日期标签文字"
infoBg: "信息背景"
infoFg: "信息文本"
infoWarnBg: "警告背景"
infoWarnFg: "警告文本"
cwBg: "CW 按钮背景"
cwFg: "CW 按钮文本"
cwHoverBg: "CW 按钮背景(悬停)"
toastBg: "吐司提示背景"
toastFg: "土司提示文本"
buttonBg: "按钮背景"
buttonHoverBg: "按钮背景(悬停)"
inputBorder: "输入框边框"
listItemHoverBg: "下拉列表项目背景(悬停)"
driveFolderBg: "驱动器文件夹背景"
wallpaperOverlay: "壁纸叠加层"
badge: "徽章"
messageBg: "聊天背景"
fgHighlighted: "高亮显示文本"
_sfx:
note: "帖子"
noteMy: "我的帖子"
@ -1121,5 +1173,5 @@ _deck:
tl: "时间线"
antenna: "天线"
list: "列表"
mentions: "提到我的"
mentions: "提"
direct: "指定用户"

View File

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

View File

@ -0,0 +1,20 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class blurhashForAvatarBanner1595077605646 implements MigrationInterface {
name = 'blurhashForAvatarBanner1595077605646'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarColor"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerColor"`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerColor" character varying(32)`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarColor" character varying(32)`);
}
}

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.41.1",
"version": "12.42.0",
"codename": "indigo",
"repository": {
"type": "git",
@ -45,9 +45,9 @@
"@fortawesome/vue-fontawesome": "0.1.10",
"@koa/cors": "3.1.0",
"@koa/multer": "3.0.0",
"@koa/router": "9.3.1",
"@koa/router": "9.0.1",
"@sinonjs/fake-timers": "6.0.1",
"@syuilo/aiscript": "0.7.2",
"@syuilo/aiscript": "0.8.0",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.14.0",
"@types/cbor": "5.0.0",
@ -112,6 +112,7 @@
"autwh": "0.1.0",
"aws-sdk": "2.713.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.3",
"bull": "3.15.0",
"cafy": "15.2.1",
"cbor": "5.0.2",

View File

@ -375,7 +375,8 @@ export default Vue.extend({
$left-widgets-hide-threshold: 1600px;
$right-widgets-hide-threshold: 1090px;
min-height: 100vh;
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box;
padding-top: $header-height;
@ -544,17 +545,14 @@ export default Vue.extend({
> .content {
> * {
min-height: calc(100vh - #{$header-height});
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
box-sizing: border-box;
padding: var(--margin);
&.full {
padding: 0 var(--margin);
}
&.naked {
background: var(--bg);
}
}
}
@ -597,7 +595,8 @@ export default Vue.extend({
&.fixed {
position: sticky;
overflow: auto;
height: calc(100vh - #{$header-height});
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc((var(--vh, 1vh) * 100) - #{$header-height});
top: $header-height;
}
@ -620,7 +619,8 @@ export default Vue.extend({
> .container {
position: sticky;
height: min-content;
min-height: calc(100vh - #{$header-height});
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
padding: var(--margin) 0;
box-sizing: border-box;

Binary file not shown.

Binary file not shown.

View File

@ -1,15 +1,9 @@
<template>
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
<span class="inner" :style="icon"></span>
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
<img class="inner" :src="url"/>
</span>
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
<span class="inner" :style="icon"></span>
</span>
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
<span class="inner" :style="icon"></span>
</router-link>
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
<span class="inner" :style="icon"></span>
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
<img class="inner" :src="url"/>
</router-link>
</template>
@ -45,22 +39,6 @@ export default Vue.extend({
? getStaticImageUrl(this.user.avatarUrl)
: this.user.avatarUrl;
},
icon(): any {
return {
backgroundColor: this.user.avatarColor,
backgroundImage: `url(${this.url})`,
};
}
},
watch: {
'user.avatarColor'() {
this.$el.style.color = this.user.avatarColor;
}
},
mounted() {
if (this.user.avatarColor) {
this.$el.style.color = this.user.avatarColor;
}
},
methods: {
onClick(e) {
@ -102,15 +80,17 @@ export default Vue.extend({
}
.inner {
background-position: center center;
background-size: cover;
position: absolute;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
border-radius: 100%;
z-index: 1;
overflow: hidden;
object-fit: cover;
width: 100%;
height: 100%;
}
}
</style>

View File

@ -95,7 +95,7 @@ export default Vue.extend({
});
if (canceled) {
if (this.column.tl == null) {
this.setType();
this.$store.commit('deviceUser/removeDeckColumn', this.column.id);
}
return;
}

View File

@ -1,7 +1,7 @@
<template>
<div class="mk-dialog" :class="{ iconOnly }">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg" ref="bg" @click="onBgClick" v-if="show"></div>
<div class="bg _modalBg" ref="bg" @click="onBgClick" v-if="show"></div>
</transition>
<transition :name="$store.state.device.animation ? 'dialog' : ''" appear @after-leave="() => { destroyDom(); }">
<div class="main" ref="main" v-if="show">
@ -245,16 +245,6 @@ export default Vue.extend({
width: initial;
}
> .bg {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
}
> .main {
display: block;
position: fixed;

View File

@ -1,36 +1,15 @@
<template>
<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`">
<img
:src="file.url"
:alt="file.name"
:title="file.name"
@load="onThumbnailLoaded"
v-if="detail && is === 'image'"/>
<video
:src="file.url"
ref="volumectrl"
preload="metadata"
controls
v-else-if="detail && is === 'video'"/>
<img :src="file.thumbnailUrl" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
<div class="zdjebgpv" ref="thumbnail">
<img-with-blurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
<audio
:src="file.url"
ref="volumectrl"
preload="metadata"
controls
v-else-if="detail && is === 'audio'"/>
<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
<fa :icon="faFile" class="icon" v-else/>
<fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/>
<fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
</div>
</template>
@ -47,8 +26,12 @@ import {
faFileArchive,
faFilm
} from '@fortawesome/free-solid-svg-icons';
import ImgWithBlurhash from './img-with-blurhash.vue';
export default Vue.extend({
components: {
ImgWithBlurhash
},
props: {
file: {
type: Object,
@ -59,11 +42,6 @@ export default Vue.extend({
required: false,
default: 'cover'
},
detail: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
@ -108,20 +86,12 @@ export default Vue.extend({
? (this.is === 'image' || this.is === 'video')
: false;
},
background(): string {
return this.file.properties.avgColor || 'transparent';
}
},
mounted() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
},
methods: {
onThumbnailLoaded() {
if (this.file.properties.avgColor) {
this.$refs.thumbnail.style.backgroundColor = 'transparent';
}
},
volumechange() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
@ -132,14 +102,8 @@ export default Vue.extend({
<style lang="scss" scoped>
.zdjebgpv {
display: flex;
position: relative;
> img,
> .icon {
pointer-events: none;
}
> .icon-sub {
position: absolute;
width: 30%;
@ -153,37 +117,10 @@ export default Vue.extend({
margin: auto;
}
&:not(.detail) {
> img {
height: 100%;
width: 100%;
object-fit: cover;
}
> .icon {
height: 65%;
width: 65%;
}
> video,
> audio {
width: 100%;
}
}
&.detail {
> .icon {
height: 100px;
width: 100px;
margin: 16px;
}
> *:not(.icon) {
max-height: 300px;
max-width: 100%;
height: 100%;
object-fit: contain;
}
> .icon {
pointer-events: none;
height: 65%;
width: 65%;
}
}
</style>

View File

@ -126,17 +126,6 @@ export default Vue.extend({
this.browser.isDragSource = false;
},
onThumbnailLoaded() {
if (this.file.properties.avgColor) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: 'transparent', // TODO fade
duration: 100,
easing: 'linear'
});
}
},
rename() {
this.$root.dialog({
title: this.$t('renameFile'),
@ -332,7 +321,6 @@ export default Vue.extend({
width: 128px;
height: 128px;
margin: auto;
color: var(--driveFileIcon);
}
> .name {

View File

@ -0,0 +1,78 @@
<template>
<div class="xubzgfgb" :title="title">
<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/>
<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { decode } from 'blurhash';
export default Vue.extend({
props: {
src: {
type: String,
required: false,
default: null
},
hash: {
type: String,
required: true
},
alt: {
type: String,
required: false,
default: '',
},
title: {
type: String,
required: false,
default: null,
},
size: {
type: Number,
required: false,
default: 64
},
},
data() {
return {
loaded: false,
};
},
mounted() {
this.draw();
},
methods: {
draw() {
const pixels = decode(this.hash, this.size, this.size);
const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
const imageData = ctx!.createImageData(this.size, this.size);
imageData.data.set(pixels);
ctx!.putImageData(imageData, 0, 0);
},
onLoad() {
this.loaded = true;
}
}
});
</script>
<style lang="scss" scoped>
.xubzgfgb {
width: 100%;
height: 100%;
> canvas,
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>

View File

@ -1,19 +1,22 @@
<template>
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="hide" @click="hide = false">
<div>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<span>{{ $t('clickToShow') }}</span>
<div class="qjewsnkg" v-if="hide" @click="hide = false">
<img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/>
<div class="text">
<div>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<span>{{ $t('clickToShow') }}</span>
</div>
</div>
</div>
<div class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else>
<div class="gqnyydlz" v-else>
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
<a
:href="image.url"
:style="style"
:title="image.name"
@click.prevent="onClick"
>
<div v-if="image.type === 'image/gif'">GIF</div>
<img-with-blurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name"/>
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
</a>
</div>
</template>
@ -23,8 +26,12 @@ import Vue from 'vue';
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { getStaticImageUrl } from '../scripts/get-static-image-url';
import ImageViewer from './image-viewer.vue';
import ImgWithBlurhash from './img-with-blurhash.vue';
export default Vue.extend({
components: {
ImgWithBlurhash
},
props: {
image: {
type: Object,
@ -42,23 +49,18 @@ export default Vue.extend({
};
},
computed: {
style(): any {
let url = `url(${
this.$store.state.device.disableShowingAnimatedImages
? getStaticImageUrl(this.image.thumbnailUrl)
: this.image.thumbnailUrl
})`;
url(): any {
let url = this.$store.state.device.disableShowingAnimatedImages
? getStaticImageUrl(this.image.thumbnailUrl)
: this.image.thumbnailUrl;
if (this.$store.state.device.loadRemoteMedia) {
url = null;
} else if (this.raw || this.$store.state.device.loadRawImages) {
url = `url(${this.image.url})`;
url = this.image.url;
}
return {
'background-color': this.image.properties.avgColor || 'transparent',
'background-image': url
};
return url;
}
},
created() {
@ -82,7 +84,38 @@ export default Vue.extend({
</script>
<style lang="scss" scoped>
.gqnyydlzavusgskkfvwvjiattxdzsqlf {
.qjewsnkg {
position: relative;
> .bg {
filter: brightness(0.5);
}
> .text {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
> div {
display: table-cell;
text-align: center;
font-size: 0.8em;
color: #fff;
> * {
display: block;
}
}
}
}
.gqnyydlz {
position: relative;
> i {
@ -110,7 +143,7 @@ export default Vue.extend({
background-size: contain;
background-repeat: no-repeat;
> div {
> .gif {
background-color: var(--fg);
border-radius: 6px;
color: var(--accentLighten);
@ -126,22 +159,4 @@ export default Vue.extend({
}
}
}
.qjewsnkgzzxlxtzncydssfbgjibiehcy {
display: flex;
justify-content: center;
align-items: center;
background: #111;
color: #fff;
> div {
display: table-cell;
text-align: center;
font-size: 12px;
> * {
display: block;
}
}
}
</style>

View File

@ -114,7 +114,7 @@ export default Vue.extend({
> * {
overflow: hidden;
border-radius: 4px;
border-radius: 6px;
}
&[data-count="1"] {

View File

@ -1,7 +1,7 @@
<template>
<div class="mk-modal" v-hotkey.global="keymap">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div>
<div class="bg _modalBg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div>
</transition>
<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div>
@ -60,13 +60,7 @@ export default Vue.extend({
.mk-modal {
> .bg {
position: fixed;
top: 0;
left: 0;
z-index: 10000;
width: 100%;
height: 100%;
background: var(--modalBg)
}
> .content {

View File

@ -1,7 +1,7 @@
<template>
<div class="mk-popup" v-hotkey.global="keymap">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg" ref="bg" @click="close()" v-if="show"></div>
<div class="bg _modalBg" ref="bg" @click="close()" v-if="show"></div>
</transition>
<transition :name="$store.state.device.animation ? 'popup' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<div class="content" :class="{ fixed }" ref="content" v-if="show" :style="{ width: width ? width + 'px' : 'auto' }"><slot></slot></div>
@ -128,13 +128,7 @@ export default Vue.extend({
.mk-popup {
> .bg {
position: fixed;
top: 0;
left: 0;
z-index: 10000;
width: 100%;
height: 100%;
background: var(--modalBg)
}
> .content {

View File

@ -1,7 +1,7 @@
<template>
<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
<div class="ulveipgl">
<transition :name="$store.state.device.animation ? 'form-fade' : ''" appear @after-leave="$emit('closed');">
<div class="bg" ref="bg" v-if="show" @click="close()"></div>
<div class="bg _modalBg" ref="bg" v-if="show" @click="close()"></div>
</transition>
<div class="main" ref="main" @click.self="close()" @keydown="onKeydown">
<transition :name="$store.state.device.animation ? 'form' : ''" appear
@ -119,16 +119,9 @@ export default Vue.extend({
opacity: 0;
}
.ulveipglmagnxfgvitaxyszerjwiqmwl {
.ulveipgl {
> .bg {
display: block;
position: fixed;
z-index: 10000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(#000, 0.7);
}
> .main {

View File

@ -1,7 +1,7 @@
<template>
<div class="mvcprjjd">
<transition name="nav-back">
<div class="nav-back"
<div class="nav-back _modalBg"
v-if="showing"
@click="showing = false"
@touchstart="showing = false"
@ -320,13 +320,7 @@ export default Vue.extend({
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
> .nav-back {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
width: 100%;
height: 100%;
background: var(--modalBg);
}
> .nav {
@ -359,7 +353,8 @@ export default Vue.extend({
left: 0;
z-index: 1001;
width: $nav-width;
height: 100vh;
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box;
overflow: auto;
background: var(--navBg);

View File

@ -61,6 +61,7 @@ export default Vue.extend({
},
methods: {
tick() {
// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
this.now = new Date();
this.tickId = setTimeout(() => {

View File

@ -0,0 +1,115 @@
<template>
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false">
<template #header>{{ title || $t('generateAccessToken') }}</template>
<div class="ugkkpisj">
<div>
<mk-info warn v-if="information">{{ information }}</mk-info>
</div>
<div>
<mk-input v-model="name">{{ $t('name') }}</mk-input>
</div>
<div>
<div style="margin-bottom: 16px;"><b>{{ $t('permission') }}</b></div>
<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button>
<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button>
<mk-switch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</mk-switch>
</div>
</div>
</x-window>
</template>
<script lang="ts">
import Vue from 'vue';
import { kinds } from '../../misc/api-permissions';
import XWindow from './window.vue';
import MkInput from './ui/input.vue';
import MkTextarea from './ui/textarea.vue';
import MkSwitch from './ui/switch.vue';
import MkButton from './ui/button.vue';
import MkInfo from './ui/info.vue';
export default Vue.extend({
components: {
XWindow,
MkInput,
MkTextarea,
MkSwitch,
MkButton,
MkInfo,
},
props: {
title: {
type: String,
required: false,
default: null
},
information: {
type: String,
required: false,
default: null
},
initialName: {
type: String,
required: false,
default: null
},
initialPermissions: {
type: Array,
required: false,
default: null
}
},
data() {
return {
name: this.initialName,
permissions: {},
kinds
};
},
created() {
if (this.initialPermissions) {
for (const kind of this.initialPermissions) {
Vue.set(this.permissions, kind, true);
}
} else {
for (const kind of this.kinds) {
Vue.set(this.permissions, kind, false);
}
}
},
methods: {
ok() {
this.$emit('ok', {
name: this.name,
permissions: Object.keys(this.permissions).filter(p => this.permissions[p])
});
this.$refs.window.close();
},
disableAll() {
for (const p in this.permissions) {
this.permissions[p] = false;
}
},
enableAll() {
for (const p in this.permissions) {
this.permissions[p] = true;
}
}
}
});
</script>
<style lang="scss" scoped>
.ugkkpisj {
> div {
padding: 24px;
border-top: solid 1px var(--divider);
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fgmtyycl _panel" :style="{ top: top + 'px', left: left + 'px' }">
<div class="fgmtyycl _panel _shadow" :style="{ top: top + 'px', left: left + 'px' }">
<mk-url-preview :url="url"/>
</div>
</template>

View File

@ -1,6 +1,6 @@
<template>
<transition name="popup" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<div v-if="show" class="fxxzrfni _panel" ref="content" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
<div v-if="show" class="fxxzrfni _panel _shadow" ref="content" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div>
<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
<div class="title">

View File

@ -49,6 +49,7 @@ import { search } from './scripts/search';
import DeckColumnCore from './components/deck/column-core.vue';
import DeckColumn from './components/deck/column.vue';
import XSidebar from './components/sidebar.vue';
import { getScrollContainer } from './scripts/scroll';
export default Vue.extend({
components: {
@ -108,6 +109,8 @@ export default Vue.extend({
created() {
document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.scrollBehavior = 'auto';
window.addEventListener('wheel', this.onWheel);
if (this.$store.getters.isSignedIn) {
this.connection = this.$root.stream.useSharedConnection('main');
@ -119,6 +122,12 @@ export default Vue.extend({
},
methods: {
onWheel(e) {
if (getScrollContainer(e.target) == null) {
document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
}
},
showNav() {
this.$refs.nav.show();
},
@ -211,7 +220,8 @@ export default Vue.extend({
--margin: var(--marginHalf);
display: flex;
height: 100vh;
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box;
flex: 1;
padding: $deckMargin 0 $deckMargin $deckMargin;

View File

@ -9,6 +9,8 @@ import PortalVue from 'portal-vue';
import VAnimateCss from 'v-animate-css';
import VueI18n from 'vue-i18n';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { AiScript } from '@syuilo/aiscript';
import { deserialize } from '@syuilo/aiscript/built/serializer';
import VueHotkey from './scripts/hotkey';
import App from './app.vue';
@ -26,7 +28,6 @@ import createStore from './store';
import { clientDb, get, count } from './db';
import { setI18nContexts } from './scripts/set-i18n-contexts';
import { createPluginEnv } from './scripts/aiscript/api';
import { AiScript } from '@syuilo/aiscript';
Vue.use(Vuex);
Vue.use(VueHotkey);
@ -59,6 +60,16 @@ if (localStorage.getItem('theme') == null) {
applyTheme(lightTheme);
}
//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
// TODO: いつの日にか消したい
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
window.addEventListener('resize', () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
});
//#endregion
//#region Detect the user language
let lang = localStorage.getItem('lang');
@ -98,44 +109,26 @@ const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
// http://qiita.com/junya/items/3ff380878f26ca447f85
document.body.setAttribute('ontouchstart', '');
// アプリ基底要素マウント
document.body.innerHTML = '<div id="app"></div>';
const store = createStore();
// 他のタブと永続化されたstateを同期
window.addEventListener('storage', e => {
if (e.key === 'vuex') {
store.replaceState({
...store.state,
...JSON.parse(e.newValue)
});
} else if (e.key === 'i') {
location.reload();
}
}, false);
const os = new MiOS(store);
os.init(async () => {
window.addEventListener('storage', e => {
if (e.key === 'vuex') {
store.replaceState(JSON.parse(localStorage['vuex']));
} else if (e.key === 'i') {
location.reload();
}
}, false);
store.watch(state => state.device.darkMode, darkMode => {
import('./scripts/theme').then(({ builtinThemes }) => {
const themes = builtinThemes.concat(store.state.device.themes);
applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme)));
});
});
//#region Sync dark mode
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: mql.matches });
}
});
//#endregion
//#region Fetch locale data
const i18n = new VueI18n();
@ -148,13 +141,6 @@ os.init(async () => {
});
//#endregion
if ('Notification' in window && store.getters.isSignedIn) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
const app = new Vue({
store: store,
i18n,
@ -228,6 +214,29 @@ os.init(async () => {
// マウント
app.$mount('#app');
store.watch(state => state.device.darkMode, darkMode => {
import('./scripts/theme').then(({ builtinThemes }) => {
const themes = builtinThemes.concat(store.state.device.themes);
applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme)));
});
});
//#region Sync dark mode
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: mql.matches });
}
});
//#endregion
store.watch(state => state.device.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
os.stream.on('emojiAdded', data => {
// TODO
//store.commit('instance/set', );
@ -259,10 +268,17 @@ os.init(async () => {
store.commit('initPlugin', { plugin, aiscript });
aiscript.exec(plugin.ast);
aiscript.exec(deserialize(plugin.ast));
}
if (store.getters.isSignedIn) {
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
const main = os.stream.useSharedConnection('main');
// 自分の情報が更新されたとき

View File

@ -1,5 +1,5 @@
<template>
<div class="naked full">
<div class="full">
<portal to="header">
<button @click="menu" class="_button _jmoebdiw_">
<fa :icon="faCloud" style="margin-right: 8px;"/>

View File

@ -30,6 +30,7 @@ import { faComments } from '@fortawesome/free-regular-svg-icons';
import Progress from '../scripts/loading';
import XTimeline from '../components/timeline.vue';
import XPostForm from '../components/post-form.vue';
import { scroll } from '../scripts/scroll';
export default Vue.extend({
metaInfo() {
@ -120,7 +121,7 @@ export default Vue.extend({
},
top() {
window.scroll({ top: 0, behavior: 'instant' });
scroll(this.$el, 0);
},
async choose(ev) {
@ -223,7 +224,7 @@ export default Vue.extend({
> i {
position: absolute;
top: 16px;
top: initial;
right: 8px;
color: var(--indicator);
font-size: 12px;

View File

@ -1,5 +1,5 @@
<template>
<div class="mk-messaging">
<div class="mk-messaging" v-size="[{ max: 400 }]">
<portal to="icon"><fa :icon="faComments"/></portal>
<portal to="title">{{ $t('messaging') }}</portal>
@ -168,18 +168,14 @@ export default Vue.extend({
.mk-messaging {
> .start {
margin: 0 auto 16px auto;
margin: 0 auto var(--margin) auto;
}
> .history {
> .message {
display: block;
text-decoration: none;
margin-bottom: 16px;
@media (max-width: 500px) {
margin-bottom: 8px;
}
margin-bottom: var(--margin);
* {
pointer-events: none;
@ -284,7 +280,7 @@ export default Vue.extend({
}
}
@media (max-width: 400px) {
&.max-width_400px {
> .history {
> .message {
&:not([data-is-me]):not([data-is-read]) {

View File

@ -10,8 +10,7 @@
<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
<div class="file" v-if="message.file">
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"
:style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/>
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
<p v-else>{{ message.file.name }}</p>
</a>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div class="mk-messaging-room naked"
<div class="mk-messaging-room"
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
@ -41,6 +41,7 @@ import XList from '../../components/date-separated-list.vue';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import parseAcct from '../../../misc/acct/parse';
import { isBottom, onScrollBottom } from '../../scripts/scroll';
export default Vue.extend({
components: {
@ -91,8 +92,6 @@ export default Vue.extend({
beforeDestroy() {
this.connection.dispose();
window.removeEventListener('scroll', this.onScroll);
document.removeEventListener('visibilitychange', this.onVisibilitychange);
this.ilObserver.disconnect();
@ -118,8 +117,6 @@ export default Vue.extend({
this.connection.on('read', this.onRead);
this.connection.on('deleted', this.onDeleted);
window.addEventListener('scroll', this.onScroll, { passive: true });
document.addEventListener('visibilitychange', this.onVisibilitychange);
this.fetchMessages().then(() => {
@ -198,7 +195,7 @@ export default Vue.extend({
onMessage(message) {
this.$root.sound('chat');
const isBottom = this.isBottom();
const _isBottom = isBottom(this.$el, 64);
this.messages.push(message);
if (message.userId != this.$store.state.i.id && !document.hidden) {
@ -207,7 +204,7 @@ export default Vue.extend({
});
}
if (isBottom) {
if (_isBottom) {
// Scroll to bottom
this.$nextTick(() => {
this.scrollToBottom();
@ -244,17 +241,6 @@ export default Vue.extend({
}
},
isBottom() {
const asobi = 64;
const current = this.isNaked
? window.scrollY + window.innerHeight
: this.$el.scrollTop + this.$el.offsetHeight;
const max = this.isNaked
? document.body.offsetHeight
: this.$el.scrollHeight;
return current > (max - asobi);
},
scrollToBottom() {
window.scroll(0, document.body.offsetHeight);
},
@ -267,6 +253,10 @@ export default Vue.extend({
notifyNewMessage() {
this.showIndicator = true;
onScrollBottom(this.$el, () => {
this.showIndicator = false;
});
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
@ -274,14 +264,6 @@ export default Vue.extend({
}, 4000);
},
onScroll() {
const el = this.isNaked ? window.document.documentElement : this.$el;
const current = el.scrollTop + el.clientHeight;
if (current > el.scrollHeight - 1) {
this.showIndicator = false;
}
},
onVisibilitychange() {
if (document.hidden) return;
for (const message of this.messages) {

View File

@ -2,9 +2,7 @@
<section class="_card">
<div class="_title"><fa :icon="faKey"/> API</div>
<div class="_content">
<mk-input :value="$store.state.i.token" readonly>
<span>{{ $t('token') }}</span>
</mk-input>
<mk-button @click="generateToken">{{ $t('generateAccessToken') }}</mk-button>
<mk-button @click="regenerateToken"><fa :icon="faSyncAlt"/> {{ $t('regenerate') }}</mk-button>
</div>
</section>
@ -26,6 +24,22 @@ export default Vue.extend({
};
},
methods: {
async generateToken() {
this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
}).$on('ok', async ({ name, permissions }) => {
const { token } = await this.$root.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
this.$root.dialog({
type: 'success',
title: this.$t('token'),
text: token
});
});
},
regenerateToken() {
this.$root.dialog({
title: this.$t('password'),

View File

@ -8,7 +8,7 @@
</template>
<section class="oyyftmcf">
<mk-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/>
<mk-file-thumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/>
</section>
</x-container>
</template>

View File

@ -68,7 +68,27 @@
</section>
<section class="_card">
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div>
<div class="_title"><fa :icon="faCog"/> {{ $t('appearance') }}</div>
<div class="_content">
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
<mk-switch v-model="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</mk-switch>
<mk-switch v-model="useOsNativeEmojis">
{{ $t('useOsNativeEmojis') }}
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</mk-switch>
</div>
<div class="_content">
<div>{{ $t('fontSize') }}</div>
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
<div class="_content">
<mk-switch v-model="autoReload">
{{ $t('autoReloadWhenDisconnected') }}
@ -76,12 +96,6 @@
</div>
<div class="_content">
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
<mk-switch v-model="useOsNativeEmojis">
{{ $t('useOsNativeEmojis') }}
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</mk-switch>
<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch>
<mk-switch v-model="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</mk-switch>
<mk-switch v-model="fixedWidgetsPosition">{{ $t('fixedWidgetsPosition') }}</mk-switch>
@ -94,13 +108,6 @@
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
</mk-select>
</div>
<div class="_content">
<div>{{ $t('fontSize') }}</div>
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
</div>
</section>
<mk-button @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</mk-button>
@ -133,6 +140,8 @@ const sounds = [
'syuilo/poi1',
'syuilo/poi2',
'syuilo/pirori',
'syuilo/pirori-wet',
'syuilo/pirori-square-wet',
'aisha/1',
'aisha/2',
'aisha/3',
@ -178,6 +187,11 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
},
useBlurEffectForModal: {
get() { return this.$store.state.device.useBlurEffectForModal; },
set(value) { this.$store.commit('device/set', { key: 'useBlurEffectForModal', value: value }); }
},
disableAnimatedMfm: {
get() { return !this.$store.state.device.animatedMfm; },
set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }

View File

@ -30,7 +30,10 @@
<div>{{ $t('description') }}:</div>
<div>{{ selectedPlugin.description }}</div>
</div>
<mk-button @click="uninstall()" style="margin-top: 8px;"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
<div style="margin-top: 8px;">
<mk-button @click="config()" inline v-if="selectedPlugin.config"><fa :icon="faCog"/> {{ $t('settings') }}</mk-button>
<mk-button @click="uninstall()" inline><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
</div>
</template>
</details>
</div>
@ -39,12 +42,13 @@
<script lang="ts">
import Vue from 'vue';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload } from '@fortawesome/free-solid-svg-icons';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkSelect from '../../components/ui/select.vue';
import MkInfo from '../../components/ui/info.vue';
import { AiScript, parse } from '@syuilo/aiscript';
export default Vue.extend({
components: {
@ -58,7 +62,7 @@ export default Vue.extend({
return {
script: '',
selectedPluginId: null,
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
@ -70,7 +74,7 @@ export default Vue.extend({
},
methods: {
install() {
async install() {
let ast;
try {
ast = parse(this.script);
@ -82,7 +86,6 @@ export default Vue.extend({
return;
}
const meta = AiScript.collectMetadata(ast);
console.log(meta);
if (meta == null) {
this.$root.dialog({
type: 'error',
@ -98,7 +101,7 @@ export default Vue.extend({
});
return;
}
const { id, name, version, author, description } = data;
const { id, name, version, author, description, permissions, config } = data;
if (id == null || name == null || version == null || author == null) {
this.$root.dialog({
type: 'error',
@ -106,16 +109,40 @@ export default Vue.extend({
});
return;
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise(async (res, rej) => {
this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
title: this.$t('tokenRequested'),
information: this.$t('pluginTokenRequestedDescription'),
initialName: name,
initialPermissions: permissions
}).$on('ok', async ({ name, permissions }) => {
const { token } = await this.$root.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
});
});
this.$store.commit('deviceUser/installPlugin', {
meta: {
id, name, version, author, description
id, name, version, author, description, permissions, config
},
ast
token,
ast: serialize(ast)
});
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$nextTick(() => {
location.reload();
});
},
uninstall() {
@ -124,6 +151,29 @@ export default Vue.extend({
type: 'success',
iconOnly: true, autoClose: true
});
this.$nextTick(() => {
location.reload();
});
},
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
async config() {
const config = this.selectedPlugin.config;
for (const key in this.selectedPlugin.configData) {
config[key].default = this.selectedPlugin.configData[key];
}
const { canceled, result } = await this.$root.form(this.selectedPlugin.name, config);
if (canceled) return;
this.$store.commit('deviceUser/configPlugin', {
id: this.selectedPluginId,
config: result
});
this.$nextTick(() => {
location.reload();
});
}
},
});

View File

@ -9,7 +9,7 @@
<mk-input v-model="author" required><span>{{ $t('author') }}</span></mk-input>
<mk-textarea v-model="description"><span>{{ $t('description') }}</span></mk-textarea>
<div class="_inputs">
<div v-text="$t('_theme.baseTheme')" />
<div v-text="$t('_theme.base')" />
<mk-radio v-model="baseTheme" value="light">{{ $t('light') }}</mk-radio>
<mk-radio v-model="baseTheme" value="dark">{{ $t('dark') }}</mk-radio>
</div>

View File

@ -1,4 +1,5 @@
import { utils, values } from '@syuilo/aiscript';
import { jsToVal } from '@syuilo/aiscript/built/interpreter/util';
export function createAiScriptEnv(vm, opts) {
let apiRequests = 0;
@ -26,7 +27,7 @@ export function createAiScriptEnv(vm, opts) {
if (token) utils.assertString(token);
apiRequests++;
if (apiRequests > 16) return values.NULL;
const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : null);
const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
return utils.jsToVal(res);
}),
'Mk:save': values.FN_NATIVE(([key, value]) => {
@ -42,8 +43,14 @@ export function createAiScriptEnv(vm, opts) {
}
export function createPluginEnv(vm, opts) {
const config = new Map();
for (const key in opts.plugin.config) {
const val = opts.plugin.configData[key] || opts.plugin.config[key].default;
config.set(key, jsToVal(val));
}
return {
...createAiScriptEnv(vm, opts),
...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }),
'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
}),
@ -53,5 +60,6 @@ export function createPluginEnv(vm, opts) {
'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
}),
'Plugin:config': values.OBJ(config),
};
}

View File

@ -25,3 +25,36 @@ export function onScrollTop(el: Element, cb) {
};
container.addEventListener('scroll', onScroll, { passive: true });
}
export function onScrollBottom(el: Element, cb) {
const container = getScrollContainer(el) || window;
const onScroll = ev => {
if (!document.body.contains(el)) return;
const pos = getScrollPosition(el);
if (pos + el.clientHeight > el.scrollHeight - 1) {
cb();
container.removeEventListener('scroll', onscroll);
}
};
container.addEventListener('scroll', onScroll, { passive: true });
}
export function scroll(el: Element, top: number) {
const container = getScrollContainer(el);
if (container == null) {
window.scroll({ top: top, behavior: 'instant' });
} else {
container.scrollTop = top;
}
}
export function isBottom(el: Element, asobi = 0) {
const container = getScrollContainer(el);
const current = container
? el.scrollTop + el.offsetHeight
: window.scrollY + window.innerHeight;
const max = container
? el.scrollHeight
: document.body.offsetHeight;
return current >= (max - asobi);
}

View File

@ -22,7 +22,7 @@ export class StickySidebar {
if (this.isTop) {
this.isTop = false;
this.spacer.style.marginTop = `${scrollTop}px`;
this.spacer.style.marginTop = `${this.lastScrollTop}px`;
}
} else { // upscroll
const overflow = this.el.clientHeight - window.innerHeight;

View File

@ -68,6 +68,7 @@ export const defaultDeviceSettings = {
disablePagesScript: true,
enableInfiniteScroll: true,
fixedWidgetsPosition: false,
useBlurEffectForModal: true,
roomGraphicsQuality: 'medium',
roomUseOrthographicCamera: true,
deckColumnAlign: 'left',
@ -301,6 +302,7 @@ export default () => new Vuex.Store({
},
mergeMe(ctx, me) {
// TODO: プロパティ一つ一つに対してコミットが発生するのはアレなので良い感じにする
for (const [key, value] of Object.entries(me)) {
ctx.commit('updateIKeyValue', { key, value });
}
@ -585,13 +587,11 @@ export default () => new Vuex.Store({
},
//#endregion
installPlugin(state, { meta, ast }) {
installPlugin(state, { meta, ast, token }) {
state.plugins.push({
id: meta.id,
name: meta.name,
version: meta.version,
author: meta.author,
description: meta.description,
...meta,
configData: {},
token: token,
ast: ast
});
},
@ -599,6 +599,10 @@ export default () => new Vuex.Store({
uninstallPlugin(state, id) {
state.plugins = state.plugins.filter(x => x.id != id);
},
configPlugin(state, { id, config }) {
state.plugins.find(p => p.id === id).configData = config;
},
}
},

View File

@ -123,10 +123,6 @@ a {
&:hover {
text-decoration: underline;
}
* {
cursor: pointer;
}
}
hr {
@ -197,6 +193,20 @@ hr {
}
}
._modalBg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--modalBg);
backdrop-filter: var(--modalBgFilter);
}
._shadow {
box-shadow: 0px 4px 32px var(--shadow) !important;
}
._button {
appearance: none;
padding: 0;

View File

@ -23,7 +23,7 @@
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
panelBorder: 'rgba(0, 0, 0, 0)',
shadow: 'rgba(0, 0, 0, 0.1)',
shadow: 'rgba(0, 0, 0, 0.3)',
header: ':alpha<0.7<@bg',
navBg: '@bg',
navFg: '@fg',
@ -57,6 +57,7 @@
badge: '#31b1ce',
messageBg: ':lighten<5<@bg',
deckColumnBorder: ':lighten<10<@panel',
htmlThemeColor: '@bg',
X1: ':alpha<0<@bg',
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',

View File

@ -57,6 +57,7 @@
badge: '#31b1ce',
messageBg: '@panel',
deckColumnBorder: ':darken<20<@panel',
htmlThemeColor: '@bg',
X1: ':alpha<0<@bg',
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',

View File

@ -12,6 +12,8 @@
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
panelBorder: '@divider',
shadow: 'rgba(255, 255, 255, 0.05)',
modalBg: 'rgba(255, 255, 255, 0.1)',
messageBg: '#1d1d1d',
deckColumnBorder: '@divider',
},

View File

@ -8,20 +8,18 @@ APIを使い始めるには、まずアクセストークンを取得する必
## アクセストークンの取得
基本的に、APIはリクエストにはアクセストークンが必要となります。
あなたの作ろうとしているアプリケーションが、あなた専用のものなのか、それとも不特定多数の人に使ってもらうものなのかによって、アクセストークンの取得手順は異なります。
APIにリクエストするのが自分自身なのか、不特定の利用者に使ってもらうアプリケーションなのかによって取得手順は異なります。
* あなた専用の場合: [「自分のアカウントのアクセストークンを取得する」](#自分のアカウントのアクセストークンを取得する)に進む
* 皆に使ってもらう場合: [「アプリケーションとしてアクセストークンを取得する」](#アプリケーションとしてアクセストークンを取得する)に進む
* 前者の場合: [「自分自身のアクセストークンを手動発行する」](#自分自身のアクセストークンを手動発行する)に進む
* 後者の場合: [「アプリケーション利用者にアクセストークンの発行をリクエストする」](#アプリケーション利用者にアクセストークンの発行をリクエストする)に進む
### 自分のアカウントのアクセストークンを取得する
「設定 > API」で、自分のアクセストークンを取得できます。
> この方法で入手したアクセストークンは強力なので、第三者に教えないでください(アプリなどにも入力しないでください)。
### 自分自身のアクセストークンを手動発行する
「設定 > API」で、自分のアクセストークンを発行できます。
[「APIの使い方」へ進む](#APIの使い方)
### アプリケーションとしてアクセストークンを取得する
アプリケーションを使ってもらうには、ユーザーのアクセストークンを以下の手順で取得する必要があります。
### アプリケーション利用者にアクセストークンの発行をリクエストする
アプリケーション利用者のアクセストークンを取得するには、以下の手順で発行をリクエストします。
#### Step 1
@ -48,7 +46,7 @@ UUIDを生成する。以後これをセッションIDと呼びます。
* どのような権限があるかは[APIリファレンス](/api-doc)で確認できます
#### Step 3
ユーザーが連携を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
ユーザーが発行を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
レスポンスに含まれるプロパティ:
* `token` ... ユーザーのアクセストークン

View File

@ -6,6 +6,7 @@ import * as fileType from 'file-type';
import isSvg from 'is-svg';
import * as probeImageSize from 'probe-image-size';
import * as sharp from 'sharp';
import { encode } from 'blurhash';
const pipeline = util.promisify(stream.pipeline);
@ -18,7 +19,7 @@ export type FileInfo = {
};
width?: number;
height?: number;
avgColor?: number[];
blurhash?: string;
warnings: string[];
};
@ -71,12 +72,11 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
}
}
// average color
let avgColor: number[] | undefined;
let blurhash: string | undefined;
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
avgColor = await calcAvgColor(path).catch(e => {
warnings.push(`calcAvgColor failed: ${e}`);
blurhash = await getBlurhash(path).catch(e => {
warnings.push(`getBlurhash failed: ${e}`);
return undefined;
});
}
@ -87,7 +87,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
type,
width,
height,
avgColor,
blurhash,
warnings,
};
}
@ -173,18 +173,15 @@ async function detectImageSize(path: string): Promise<{
/**
* Calculate average color of image
*/
async function calcAvgColor(path: string): Promise<number[]> {
const img = sharp(path);
const info = await (img as any).stats();
if (info.isOpaque) {
const r = Math.round(info.channels[0].mean);
const g = Math.round(info.channels[1].mean);
const b = Math.round(info.channels[2].mean);
return [r, g, b];
} else {
return [255, 255, 255];
}
function getBlurhash(path: string): Promise<string> {
return new Promise((resolve, reject) => {
sharp(path)
.raw()
.ensureAlpha()
.resize(64, 64, { fit: 'inside' })
.toBuffer((err, buffer, { width, height }) => {
if (err) return reject(err);
resolve(encode(new Uint8ClampedArray(buffer), width, height, 7, 7));
});
});
}

View File

@ -67,6 +67,12 @@ export class DriveFile {
})
public comment: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The BlurHash string.'
})
public blurhash: string | null;
@Column('jsonb', {
default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'

View File

@ -106,14 +106,14 @@ export class User {
public bannerUrl: string | null;
@Column('varchar', {
length: 32, nullable: true,
length: 128, nullable: true,
})
public avatarColor: string | null;
public avatarBlurhash: string | null;
@Column('varchar', {
length: 32, nullable: true,
length: 128, nullable: true,
})
public bannerColor: string | null;
public bannerBlurhash: string | null;
@Column('boolean', {
default: false,

View File

@ -115,6 +115,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: file.properties,
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
thumbnailUrl: this.getPublicUrl(file, true, meta),

View File

@ -165,7 +165,8 @@ export class UserRepository extends Repository<User> {
username: user.username,
host: user.host,
avatarUrl: user.avatarUrl ? user.avatarUrl : config.url + '/avatar/' + user.id,
avatarColor: user.avatarColor,
avatarBlurhash: user.avatarBlurhash,
avatarColor: null, // 後方互換性のため
isAdmin: user.isAdmin || falsy,
isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy,
@ -196,7 +197,8 @@ export class UserRepository extends Repository<User> {
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
bannerUrl: user.bannerUrl,
bannerColor: user.bannerColor,
bannerBlurhash: user.bannerBlurhash,
bannerColor: null, // 後方互換性のため
isLocked: user.isLocked,
isModerator: user.isModerator || falsy,
isSilenced: user.isSilenced || falsy,
@ -331,7 +333,7 @@ export const packedUserSchema = {
format: 'url',
nullable: true as const, optional: false as const,
},
avatarColor: {
avatarBlurhash: {
type: 'any' as const,
nullable: true as const, optional: false as const,
},
@ -340,7 +342,7 @@ export const packedUserSchema = {
format: 'url',
nullable: true as const, optional: true as const,
},
bannerColor: {
bannerBlurhash: {
type: 'any' as const,
nullable: true as const, optional: true as const,
},

View File

@ -47,7 +47,15 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
try {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
} catch (e) {
// 対象が4xxならスキップ
if (e.statusCode >= 400 && e.statusCode < 500) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
}
throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
}
}
// それでもわからなければ終了

View File

@ -226,24 +226,24 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null;
const avatarBlurhash = avatar ? avatar.blurhash : null;
const bannerBlurhash = banner ? banner.blurhash : null;
await Users.update(user!.id, {
avatarId,
bannerId,
avatarUrl,
bannerUrl,
avatarColor,
bannerColor
avatarBlurhash,
bannerBlurhash
});
user!.avatarId = avatarId;
user!.bannerId = bannerId;
user!.avatarUrl = avatarUrl;
user!.bannerUrl = bannerUrl;
user!.avatarColor = avatarColor;
user!.bannerColor = bannerColor;
user!.avatarBlurhash = avatarBlurhash;
user!.bannerBlurhash = bannerBlurhash;
//#endregion
//#region カスタム絵文字取得
@ -341,13 +341,13 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
if (avatar) {
updates.avatarId = avatar.id;
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
updates.avatarBlurhash = avatar.blurhash;
}
if (banner) {
updates.bannerId = banner.id;
updates.bannerUrl = DriveFiles.getPublicUrl(banner);
updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null;
updates.bannerBlurhash = banner.blurhash;
}
// Update user

View File

@ -210,8 +210,8 @@ export default define(meta, async (ps, user, token) => {
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
if (avatar.properties.avgColor) {
updates.avatarColor = avatar.properties.avgColor;
if (avatar.blurhash) {
updates.avatarBlurhash = avatar.blurhash;
}
}
@ -223,8 +223,8 @@ export default define(meta, async (ps, user, token) => {
updates.bannerUrl = DriveFiles.getPublicUrl(banner, false);
if (banner.properties.avgColor) {
updates.bannerColor = banner.properties.avgColor;
if (banner.blurhash) {
updates.bannerBlurhash = banner.blurhash;
}
}

View File

@ -13,7 +13,7 @@ export const meta = {
params: {
session: {
validator: $.str
validator: $.nullable.str
},
name: {
@ -52,4 +52,8 @@ export default define(meta, async (ps, user) => {
iconUrl: ps.iconUrl,
permission: ps.permission,
});
return {
token: accessToken
};
});

View File

@ -78,7 +78,7 @@ router.post('/miauth/:session/check', async ctx => {
session: ctx.params.session
});
if (token && !token.fetched) {
if (token && token.session != null && !token.fetched) {
AccessTokens.update(token.id, {
fetched: true
});

View File

@ -1,6 +1,6 @@
import endpoints from '../endpoints';
import * as locale from '../../../../locales/';
import { kinds as kindsList } from '../kinds';
import { kinds as kindsList } from '../../../misc/api-permissions';
export interface IKindInfo {
endpoints: string[];

View File

@ -40,7 +40,7 @@ html
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
if (k === 'html') {
if (k === 'htmlThemeColor') {
for (const tag of document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', v);
@ -61,7 +61,8 @@ html
document.documentElement.style.backgroundImage = `url(${wallpaper})`;
}
body
//- https://qiita.com/junya/items/3ff380878f26ca447f85
body(ontouchstart='')
noscript: p
| JavaScriptを有効にしてください
br

View File

@ -327,7 +327,6 @@ export default async function(
const properties: {
width?: number;
height?: number;
avgColor?: string;
} = {};
if (info.width) {
@ -335,10 +334,6 @@ export default async function(
properties['height'] = info.height;
}
if (info.avgColor) {
properties['avgColor'] = `rgb(${info.avgColor.join(',')})`;
}
const profile = user ? await UserProfiles.findOne(user.id) : null;
const folder = await fetchFolder();
@ -351,6 +346,7 @@ export default async function(
file.folderId = folder !== null ? folder.id : null;
file.comment = comment;
file.properties = properties;
file.blurhash = info.blurhash || null;
file.isLink = isLink;
file.isSensitive = user
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :

View File

@ -26,7 +26,7 @@ describe('Get file info', () => {
},
width: undefined,
height: undefined,
avgColor: undefined
blurhash: null
});
}));
@ -43,7 +43,7 @@ describe('Get file info', () => {
},
width: 512,
height: 512,
avgColor: [ 181, 99, 106 ]
blurhash: '' // TODO
});
}));
@ -60,7 +60,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
avgColor: [ 249, 253, 250 ]
blurhash: '' // TODO
});
}));
@ -77,7 +77,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
avgColor: [ 249, 253, 250 ]
blurhash: '' // TODO
});
}));
@ -94,7 +94,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
avgColor: [ 255, 255, 255 ]
blurhash: '' // TODO
});
}));
@ -111,7 +111,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
avgColor: [ 255, 255, 255 ]
blurhash: '' // TODO
});
}));
@ -129,7 +129,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
avgColor: [ 255, 255, 255 ]
blurhash: '' // TODO
});
}));
@ -146,7 +146,7 @@ describe('Get file info', () => {
},
width: 25000,
height: 25000,
avgColor: undefined
blurhash: '' // TODO
});
}));
});

View File

@ -160,10 +160,10 @@
resolved "https://registry.yarnpkg.com/@koa/multer/-/multer-3.0.0.tgz#439777949f28097d7b329c0b4ce3048074c862f8"
integrity sha512-y+OQBmex5D1jIl723gAEUYcAWPEicIXppaAKw/zCMfpllQ08ZNweDPwoCLxEoatqd5pCu2XG6V8dl67JRq3RJw==
"@koa/router@9.3.1":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@koa/router/-/router-9.3.1.tgz#814b0f357da616b99ee22259644cd928f2c9e60e"
integrity sha512-OOy4pOEO+Zz5vy+zqc8mWRGKYIpDqjgbVTF/U41fCwBwVWHGmkedvcJ9V5MLI7Ivy0iTv8o0XLDtGWtYHquvxg==
"@koa/router@9.0.1":
version "9.0.1"
resolved "https://registry.yarnpkg.com/@koa/router/-/router-9.0.1.tgz#4090a14223ea7e78aa13b632761209cba69acd95"
integrity sha512-OI+OU49CJV4px0WkIMmayBeqVXB/JS1ZMq7UoGlTZt6Y7ijK7kdeQ18+SEHHJPytmtI1y6Hf8XLrpxva3mhv5Q==
dependencies:
debug "^4.1.1"
http-errors "^1.7.3"
@ -192,10 +192,10 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@syuilo/aiscript@0.7.2":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.7.2.tgz#2f30adb14ffa9f1180af83c059927ab306b175a5"
integrity sha512-l8HVTJTq9KLzDqGswOIGlBepkacudUp70EScrLjL7nEL2NKcti7Ui5fwZCrmxazxgGz6NrVNX5UBIOFFyrwr0A==
"@syuilo/aiscript@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.8.0.tgz#3a895ddd9f5bd5afa1648acb5fd3e6f94f434cbb"
integrity sha512-mrZ3awYf1R81D+OWZctRFiAWUt6xL3A5ovBn2OD8+1hZyX3T7S+awqrhYVLoQPhd/cijz1RT6PE8AEUtuR1J8Q==
dependencies:
autobind-decorator "2.4.0"
chalk "4.0.0"
@ -1669,6 +1669,11 @@ bluebird@^3.1.1, bluebird@^3.4.1:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
blurhash@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
bn.js@^4.0.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"