Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e9717a5fc | |||
3e28b296e3 | |||
056fef70da | |||
55be9cc9d1 | |||
3f2ffcea97 | |||
b07d037cb5 | |||
4feccdfd92 | |||
f1ef85b636 | |||
cf9266eab9 | |||
4a1552fb3c | |||
e5863c2867 | |||
58211fc6a7 | |||
bd54e44b35 | |||
e1f2e364a4 | |||
186b26e103 | |||
74706a8d2c | |||
7e2b6b6369 | |||
1a2de1a051 | |||
83900cbca6 | |||
166bc19131 | |||
da874f3383 | |||
9c30b23358 | |||
b8350d5093 | |||
58f7af8927 | |||
c3b9c7b74b | |||
9415618992 | |||
1c200c9b94 | |||
ce8fa8e423 | |||
a4b7a9db03 | |||
280eeb9d75 | |||
3f71b14637 | |||
705d40ab37 | |||
b39850de01 | |||
b9c5e95b85 | |||
0c1de7b1b6 | |||
0a4499fd03 | |||
b663a47331 | |||
eb275a62a6 | |||
e18caa3396 | |||
eb15d31ebf | |||
e7f1ab2d01 | |||
9d3beb3174 | |||
b6c3399abe | |||
0a28573845 | |||
937df577f1 | |||
e54fd6c2cb | |||
3caea9d33e | |||
b9e9631195 | |||
76bded455a | |||
c94d9210ed | |||
0f5db9558c | |||
35a8c37922 | |||
6ff84a1061 | |||
aae9bc4cf4 | |||
426c2fa5d1 | |||
dab728278c | |||
7555ab097a | |||
5b5b64d251 | |||
eb84445796 | |||
364bd9ae74 | |||
d4b0761549 |
78
CHANGELOG.md
78
CHANGELOG.md
@ -1,5 +1,83 @@
|
||||
ChangeLog
|
||||
=========
|
||||
12.42.0 (2020/7/19)
|
||||
-------------------
|
||||
*このアップデートでは、データベースのマイグレーション(`npm run migrate`/`yarn migrate`)が必要です。*
|
||||
|
||||
### ✨Improvements
|
||||
- Deckでマウスホイールを使って横スクロールできるように [e18caa3](https://github.com/syuilo/misskey/commit/e18caa339624b566e76d19d0e132028b6377f7f8), [eb275a6](https://github.com/syuilo/misskey/commit/eb275a62a6ae5699b38cf3bca516d34b44e9d944)
|
||||
- 設定画面を整理 [b663a47](https://github.com/syuilo/misskey/commit/b663a47331000b61010ad91fdc422b60b2eeb660)
|
||||
* アクセシビリティ → アピアランス
|
||||
- リモートで削除されており、ローカルで削除されている若しくは未認知のActorからActivityを受信した場合に、エラーでリトライしないように [#6554](https://github.com/syuilo/misskey/pull/6554)
|
||||
- トークン手動発行機能を実装 [0c1de7b](https://github.com/syuilo/misskey/commit/0c1de7b1b6e9ac10b62d8b3157cb064c79aa21d1), [b9c5e95](https://github.com/syuilo/misskey/commit/b9c5e95b855fcf599b339037d4753252a1f786d4)
|
||||
- AiScriptからAPIにアクセスできるように [b39850d](https://github.com/syuilo/misskey/commit/b39850de012fa7b05959c7f4bbbbade841d186ff)
|
||||
- Blurhashを実装 [3f71b14](https://github.com/syuilo/misskey/commit/3f71b1463719bee476d39b7ceca5a2eea4b5cb67)
|
||||
* avgColor, avatarColor, bannerColor は使われなくなります。
|
||||
- デザイン・挙動の調整 [280eeb9](https://github.com/syuilo/misskey/commit/280eeb9d7539e5b7c8d09dfa21a7679eebb09407)
|
||||
|
||||
### 🐛Fixes
|
||||
- AiScriptのアップデートとプラグインの動作の修正 [705d40a](https://github.com/syuilo/misskey/commit/705d40ab37bedb1e43e4677457497c342517a23d)
|
||||
|
||||
12.41.3 (2020/7/15)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
- サウンドを追加 [b9e9631](https://github.com/syuilo/misskey/commit/b9e9631195a8ca5ed1386daeacdc835456d52975)
|
||||
|
||||
### 🐛Fixes
|
||||
- サイドバーのsticky動作の修正 [937df57](https://github.com/syuilo/misskey/commit/937df577f1b005ff4da2122e642c5c9f687d0069)
|
||||
- iOS/macOS Safariで投稿フォームやノートメニューをたまに表示できない問題を修正 [9d3beb3](https://github.com/syuilo/misskey/commit/9d3beb3174f87f05c50e2e7304a03d2c55a3f7ec)
|
||||
* windowのstorageイベントが発火すると永続化されたstateのみが残る問題を修正
|
||||
- iOS Safariで設定を選べなくなってしまうことがある問題を修正 [e7f1ab2](https://github.com/syuilo/misskey/commit/e7f1ab2d01f92558ff5e230663d951686390d35a)
|
||||
* Safariのvh計算のバグに対処
|
||||
|
||||
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)
|
||||
-------------------
|
||||
|
@ -1,6 +1,12 @@
|
||||
# Contribution guide
|
||||
:v: Thanks for your contributions :v:
|
||||
|
||||
## When you contribute...
|
||||
- 任意のIssueについて、せっかく実装してくださっても、実装方法や設計の認識が揃ってないとマージできない/しないことになりかねないので、初めにそのIssue上で着手することを宣言し、必要に応じて他メンバーと実装方法や設計のすり合わせを行ってください。宣言することは作業が他の人と被るのを防止する効果もあります。
|
||||
- 設計に迷った時はプロジェクトリーダーの判断を仰いでください。
|
||||
- 時間や優先度の都合上、提出してくださったPRが長期間放置されることもありますがご理解ください。
|
||||
- 温度感高めで見てほしいものは責付いてください。
|
||||
|
||||
## Issues
|
||||
Feature suggestions and bug reports are filed in https://github.com/syuilo/misskey/issues .
|
||||
|
||||
|
@ -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: "إلغاء الكتم"
|
||||
@ -189,6 +193,7 @@ rename: "إعادة التسمية"
|
||||
avatar: "الصورة الرمزية"
|
||||
banner: "الصورة الرأسية"
|
||||
nsfw: "محتوى حساس"
|
||||
disconnectedFromServer: "قُطِع الإتصال بالخادم"
|
||||
reload: "انعش"
|
||||
doNothing: "تجاهل"
|
||||
watch: "راقب"
|
||||
@ -227,6 +232,7 @@ manageAntennas: "إدارة الهوائيات"
|
||||
name: "الإسم"
|
||||
antennaSource: "مصدر الهوائي"
|
||||
antennaKeywords: "الكلمات المفتاحية للإستقبال"
|
||||
withReplies: "بالردود"
|
||||
notesAndReplies: "الملاحظات والردود"
|
||||
withFiles: "بالمرفقات"
|
||||
silence: "اكتم"
|
||||
@ -250,6 +256,8 @@ unregister: "إلغاء التسجيل"
|
||||
passwordLessLogin: "لِج مِن دون كلمة سرية"
|
||||
resetPassword: "أعد تعيين كلمتك السرية"
|
||||
newPasswordIs: "كلمتك السرية الجديدة هي {password}"
|
||||
autoReloadWhenDisconnected: "إنعاش تلقائي عندما يُقطَع الإتصال بالخادم"
|
||||
autoNoteWatch: "راقب الملاحظات تلقائيا"
|
||||
share: "شارِك"
|
||||
notFound: "غير موجود"
|
||||
help: "المساعدة"
|
||||
@ -271,6 +279,8 @@ next: "التالية"
|
||||
retype: "أعد الكتابة"
|
||||
noteOf: "ملاحظات {user}"
|
||||
inviteToGroup: "دعوة إلى فريق"
|
||||
noMessagesYet: "ليس هناك رسائل بعد"
|
||||
newMessageExists: "لقد تلقيت رسالة جديدة"
|
||||
invitationCode: "رمز الدعوة"
|
||||
checking: "التحقق جارٍ"
|
||||
available: "متوفر"
|
||||
@ -288,6 +298,7 @@ uiLanguage: "لغة واجهة المستخدم"
|
||||
aboutX: "عن {x}"
|
||||
useOsNativeEmojis: "استخدم الإيموجيات الخاصة بنظام التشغيل"
|
||||
youHaveNoGroups: "لا تمتلك أية فِرَق"
|
||||
noHistory: "السجل فارغ"
|
||||
doing: "انتظر لحظة"
|
||||
category: "الفئات"
|
||||
tags: "الوسوم"
|
||||
@ -302,6 +313,7 @@ remote: "بُعدي"
|
||||
total: "المجموع"
|
||||
weekOverWeekChanges: "أسبوعيا"
|
||||
dayOverDayChanges: "يوميا"
|
||||
appearance: "المظهر"
|
||||
clinetSettings: "إعدادات التطبيق"
|
||||
accountSettings: "إعدادات الحساب"
|
||||
promotion: "ترقية"
|
||||
@ -330,10 +342,28 @@ rooms: "الغرفة"
|
||||
relays: "المُرَحلات"
|
||||
addRelay: "إضافة مُرحّل"
|
||||
addedRelays: "المرحلات التي تم إضافتها"
|
||||
deletedNote: "ملاحظة محذوفة"
|
||||
invisibleNote: "ملاحظة مخفية"
|
||||
poll: "استطلاع رأي"
|
||||
themeEditor: "مصمم القوالب"
|
||||
plugins: "الإضافات"
|
||||
pluginInstallWarn: "يرجى تنصيب إضافات ذات مصدر موثوق منه فقط."
|
||||
smtpHost: "المضيف"
|
||||
smtpUser: "اسم المستخدم"
|
||||
smtpPass: "الكلمة السرية"
|
||||
_theme:
|
||||
explore: "استكشف قوالب المظهر"
|
||||
install: "تنصيب قالب"
|
||||
manage: "إدارة القوالب"
|
||||
code: "شيفرة القالب"
|
||||
installed: "تم تنصيب {name}"
|
||||
make: "إنشاء قالب"
|
||||
alpha: "الشفافية"
|
||||
keys:
|
||||
messageBg: "خلفية الدردشة"
|
||||
_sfx:
|
||||
note: "الملاحظات"
|
||||
noteMy: "ملاحظتي"
|
||||
notification: "الإشعارات"
|
||||
chat: "الدردشة"
|
||||
_ago:
|
||||
@ -378,18 +408,29 @@ _widgets:
|
||||
rss: "تدفق RSS"
|
||||
activity: "النشاط"
|
||||
photos: "الصور"
|
||||
federation: "الفديرالية"
|
||||
_cw:
|
||||
hide: "إخفاء"
|
||||
show: "عرض المزيد"
|
||||
chars: "{count} أحرف"
|
||||
files: "{count} ملفات"
|
||||
_poll:
|
||||
noOnlyOneChoice: "تحتاج إلى خيارَين على الأقل"
|
||||
choiceN: "الخيار {n}"
|
||||
noMore: "لا يمكنك إضافة خيارات أخرى"
|
||||
canMultipleVote: "السماح بالإجابات المتعددة"
|
||||
expiration: "ينتهي استطلاع الرأي في"
|
||||
infinite: "أبدًا"
|
||||
at: "تاريخ الإنتهاء"
|
||||
after: "ينتهي بعد…"
|
||||
deadlineDate: "تاريخ الانتهاء"
|
||||
deadlineTime: "سا"
|
||||
duration: "المدة"
|
||||
votesCount: "{n} أصوات"
|
||||
totalVotes: "المجموع {n} أصوات"
|
||||
vote: "قم بالتصويت"
|
||||
showResult: "اعرض النتائج"
|
||||
voted: "تم التصويت"
|
||||
closed: "انتهى"
|
||||
remainingDays: "{d} أيام و {h} ساعات متبقية"
|
||||
remainingHours: "{h} ساعات و {m} دقائق متبقية"
|
||||
@ -409,6 +450,7 @@ _profile:
|
||||
username: "اسم المستخدم"
|
||||
youCanIncludeHashtags: "يمكنك أيضًا إضافة وسوم إلى نبذتك التعريفية."
|
||||
_exportOrImport:
|
||||
allNotes: "كل الملاحظات"
|
||||
followingList: "المتابَعون"
|
||||
muteList: "اكتم"
|
||||
blockingList: "احجب"
|
||||
@ -426,6 +468,7 @@ _rooms:
|
||||
default: "افتراضي"
|
||||
_furnitures:
|
||||
monitor: "شاشة التحكم"
|
||||
banknote: "أوراق نقدية"
|
||||
_pages:
|
||||
blocks:
|
||||
image: "الصور"
|
||||
@ -453,6 +496,9 @@ _pages:
|
||||
types:
|
||||
array: "القوائم"
|
||||
_notification:
|
||||
youGotPoll: "شارك {name} في استطلاع الرأي"
|
||||
youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
|
||||
youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
|
||||
youWereFollowed: "يتابعك"
|
||||
_deck:
|
||||
_columns:
|
||||
|
@ -442,7 +442,7 @@ remote: "Fremd"
|
||||
total: "Gesamt"
|
||||
weekOverWeekChanges: "Wöchentlich"
|
||||
dayOverDayChanges: "Täglich"
|
||||
accessibility: "Barrierefreiheit"
|
||||
appearance: "Aussehen"
|
||||
clinetSettings: "Client-Einstellungen"
|
||||
accountSettings: "Benutzerkonto-Einstellungen"
|
||||
promotion: "Hervorgehoben"
|
||||
@ -523,8 +523,21 @@ 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"
|
||||
useBlurEffectForModal: "Weichzeichnungseffekt für Modals verwenden"
|
||||
generateAccessToken: "Zugriffstoken generieren"
|
||||
permission: "Berechtigungen"
|
||||
enableAll: "Alle aktivieren"
|
||||
disableAll: "Alle deaktivieren"
|
||||
tokenRequested: "Benutzerkontozugriff gewähren"
|
||||
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
|
||||
smtpHost: "Host"
|
||||
smtpUser: "Benutzername"
|
||||
smtpPass: "Passwort"
|
||||
_theme:
|
||||
explore: "Themen erforschen"
|
||||
install: "Thema installieren"
|
||||
@ -545,7 +558,7 @@ _theme:
|
||||
func: "Funktionen"
|
||||
funcKind: "Funktionstyp"
|
||||
argument: "Parameter"
|
||||
basedProp: "Name der referenzierten Eigenschaft"
|
||||
basedProp: "Referenzierte Eigenschaft"
|
||||
alpha: "Transparenz"
|
||||
darken: "Verdunkeln"
|
||||
lighten: "Erhellen"
|
||||
@ -710,6 +723,7 @@ _widgets:
|
||||
activity: "Aktivität"
|
||||
photos: "Fotos"
|
||||
digitalClock: "Digitaluhr"
|
||||
federation: "Föderation"
|
||||
_cw:
|
||||
hide: "Ausblenden"
|
||||
show: "Mehr anzeigen"
|
||||
@ -1166,6 +1180,7 @@ _notification:
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Hauptspalte immer zeigen"
|
||||
columnAlign: "Spalten ausrichten"
|
||||
addColumn: "Spalte hinzufügen"
|
||||
_columns:
|
||||
widgets: "Widgets"
|
||||
notifications: "Benachrichtigungen"
|
||||
|
@ -442,7 +442,7 @@ remote: "Remote"
|
||||
total: "Total"
|
||||
weekOverWeekChanges: "Weekly"
|
||||
dayOverDayChanges: "Daily"
|
||||
accessibility: "Accessibility"
|
||||
appearance: "Appearance"
|
||||
clinetSettings: "Client Settings"
|
||||
accountSettings: "Account Settings"
|
||||
promotion: "Promoted"
|
||||
@ -523,8 +523,21 @@ 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"
|
||||
useBlurEffectForModal: "Use blur effect for modals"
|
||||
generateAccessToken: "Generate access token"
|
||||
permission: "Permissions"
|
||||
enableAll: "Enable all"
|
||||
disableAll: "Disable all"
|
||||
tokenRequested: "Grant access to account"
|
||||
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
|
||||
smtpHost: "Host"
|
||||
smtpUser: "Username"
|
||||
smtpPass: "Password"
|
||||
_theme:
|
||||
explore: "Explore Themes"
|
||||
install: "Install theme"
|
||||
@ -545,7 +558,7 @@ _theme:
|
||||
func: "Functions"
|
||||
funcKind: "Function type"
|
||||
argument: "Argument"
|
||||
basedProp: "Name of the referenced property"
|
||||
basedProp: "Referenced property"
|
||||
alpha: "Opacity"
|
||||
darken: "Darken"
|
||||
lighten: "Lighten"
|
||||
@ -710,6 +723,7 @@ _widgets:
|
||||
activity: "Activity"
|
||||
photos: "Photos"
|
||||
digitalClock: "Digital clock"
|
||||
federation: "Federation"
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
show: "Load more"
|
||||
@ -1166,6 +1180,7 @@ _notification:
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Always show main column"
|
||||
columnAlign: "Align columns"
|
||||
addColumn: "Add column"
|
||||
_columns:
|
||||
widgets: "Widgets"
|
||||
notifications: "Notifications"
|
||||
|
@ -442,7 +442,7 @@ remote: "Remoto"
|
||||
total: "Total"
|
||||
weekOverWeekChanges: "Dif semanal"
|
||||
dayOverDayChanges: "Dif diaria"
|
||||
accessibility: "Accesibilidad"
|
||||
appearance: "Apariencia"
|
||||
clinetSettings: "Ajustes del cliente"
|
||||
accountSettings: "Ajustes de cuenta"
|
||||
promotion: "Promovido"
|
||||
@ -523,6 +523,32 @@ 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"
|
||||
deck: "Deck"
|
||||
undeck: "Quitar deck"
|
||||
useBlurEffectForModal: "Usar efecto borroso en modales"
|
||||
generateAccessToken: "Generar token de acceso"
|
||||
permission: "Permisos"
|
||||
enableAll: "Activar todo"
|
||||
disableAll: "Desactivar todo"
|
||||
tokenRequested: "Permiso de acceso a la cuenta"
|
||||
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
|
||||
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
|
||||
emailConfig: "Configuración del servidor de correos"
|
||||
enableEmail: "Activar el envío de correos electrónicos"
|
||||
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
|
||||
email: "Correo electrónico"
|
||||
smtpConfig: "Configuración del servidor SMTP"
|
||||
smtpHost: "Host"
|
||||
smtpPort: "Puerto"
|
||||
smtpUser: "Nombre de usuario"
|
||||
smtpPass: "Contraseña"
|
||||
emptyToDisableSmtpAuth: "Deje el nombre del usuario y la contraseña en blanco para deshabilitar la autenticación SMTP"
|
||||
smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP"
|
||||
smtpSecureInfo: "Apagar cuando se use STARTTLS"
|
||||
testEmail: "Prueba de envío"
|
||||
_theme:
|
||||
explore: "Explorar temas"
|
||||
install: "Instalar tema"
|
||||
@ -573,6 +599,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 +733,8 @@ _widgets:
|
||||
rss: "Lector RSS"
|
||||
activity: "Actividad"
|
||||
photos: "Fotos"
|
||||
digitalClock: "Reloj digital"
|
||||
federation: "Federación"
|
||||
_cw:
|
||||
hide: "Ocultar"
|
||||
show: "Ver más"
|
||||
@ -1140,7 +1189,11 @@ _notification:
|
||||
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
|
||||
youWereInvitedToGroup: "Invitado al grupo"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Siempre mostrar la columna principal"
|
||||
columnAlign: "Alinear columnas"
|
||||
addColumn: "Agregar columna"
|
||||
_columns:
|
||||
widgets: "Widgets"
|
||||
notifications: "Notificaciones"
|
||||
tl: "Linea de tiempo"
|
||||
antenna: "Antenas"
|
||||
|
@ -442,7 +442,7 @@ remote: "Distant"
|
||||
total: "Total"
|
||||
weekOverWeekChanges: "Diff hebdo"
|
||||
dayOverDayChanges: "Diff quotidien"
|
||||
accessibility: "Accessibilité"
|
||||
appearance: "Aspect"
|
||||
clinetSettings: "Paramètres du client"
|
||||
accountSettings: "Paramètres du compte"
|
||||
promotion: "Promu"
|
||||
@ -516,6 +516,19 @@ 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"
|
||||
leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer ?"
|
||||
manage: "Gestion"
|
||||
plugins: "Extensions"
|
||||
pluginInstallWarn: "N’installez que des extensions provenant de sources de confiance."
|
||||
smtpHost: "Hôte"
|
||||
smtpUser: "Nom d’utilisateur·rice"
|
||||
smtpPass: "Mot de passe"
|
||||
_theme:
|
||||
explore: "Explorer les thèmes"
|
||||
install: "Installer un thème"
|
||||
@ -524,12 +537,35 @@ _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"
|
||||
key: "Clé "
|
||||
func: "Fonction"
|
||||
argument: "Argument"
|
||||
alpha: "Transparence"
|
||||
darken: "Assombrir"
|
||||
importInfo: "Vous pouvez importer un thème vers l’éditeur de thèmes en saisissant son code ici."
|
||||
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"
|
||||
navFg: "Texte de la barre latérale"
|
||||
link: "Lien"
|
||||
hashtag: "Hashtags"
|
||||
mention: "Mentionner"
|
||||
mentionMe: "Mentions (Moi)"
|
||||
renote: "Renote"
|
||||
divider: "Séparateur"
|
||||
infoWarnFg: "Texte d’avertissement"
|
||||
badge: "Badge"
|
||||
messageBg: "Arrière plan de la discussion"
|
||||
_sfx:
|
||||
note: "Nouvelle note"
|
||||
noteMy: "Ma note"
|
||||
@ -643,6 +679,8 @@ _widgets:
|
||||
rss: "Lecteur de flux RSS"
|
||||
activity: "Activité"
|
||||
photos: "Photos"
|
||||
digitalClock: "Horloge numérique"
|
||||
federation: "Fédération"
|
||||
_cw:
|
||||
hide: "Masquer"
|
||||
show: "Afficher plus …"
|
||||
@ -891,7 +929,7 @@ _pages:
|
||||
pushEvent: "Envoyer un évènement"
|
||||
_pushEvent:
|
||||
event: "Nom de l’évènement"
|
||||
message: "Message à afficher lorsque appuyé"
|
||||
message: "Message à afficher lorsqu’il est activé"
|
||||
variable: "Variable à envoyer"
|
||||
no-variable: "Rien"
|
||||
callAiScript: "Appeler AiScript"
|
||||
@ -1097,7 +1135,11 @@ _notification:
|
||||
yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté"
|
||||
youWereInvitedToGroup: "Invité au groupe"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Toujours afficher la colonne principale"
|
||||
columnAlign: "Aligner les colonnes"
|
||||
addColumn: "Ajouter une colonne"
|
||||
_columns:
|
||||
widgets: "Widgets"
|
||||
notifications: "Notifications"
|
||||
tl: "Fil"
|
||||
antenna: "Antennes"
|
||||
|
@ -104,6 +104,8 @@ unblockConfirm: "ブロック解除しますか?"
|
||||
suspendConfirm: "凍結しますか?"
|
||||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
selectAntenna: "アンテナを選択"
|
||||
selectWidget: "ウィジェットを選択"
|
||||
customEmojis: "カスタム絵文字"
|
||||
emoji: "絵文字"
|
||||
emojiName: "絵文字名"
|
||||
@ -442,7 +444,7 @@ remote: "リモート"
|
||||
total: "合計"
|
||||
weekOverWeekChanges: "前週比"
|
||||
dayOverDayChanges: "前日比"
|
||||
accessibility: "アクセシビリティ"
|
||||
appearance: "アピアランス"
|
||||
clinetSettings: "クライアント設定"
|
||||
accountSettings: "アカウント設定"
|
||||
promotion: "プロモーション"
|
||||
@ -528,6 +530,29 @@ plugins: "プラグイン"
|
||||
pluginInstallWarn: "信頼できないプラグインはインストールしないでください。"
|
||||
deck: "デッキ"
|
||||
undeck: "デッキ解除"
|
||||
useBlurEffectForModal: "モーダルにぼかし効果を使用"
|
||||
generateAccessToken: "アクセストークンの発行"
|
||||
permission: "権限"
|
||||
enableAll: "全て有効にする"
|
||||
disableAll: "全て無効にする"
|
||||
tokenRequested: "アカウントへのアクセス許可"
|
||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
|
||||
notificationType: "通知の種類"
|
||||
edit: "編集"
|
||||
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
|
||||
emailConfig: "メールサーバー設定"
|
||||
enableEmail: "メール配信機能を有効化する"
|
||||
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
||||
email: "メールアドレス"
|
||||
smtpConfig: "SMTP サーバーの設定"
|
||||
smtpHost: "ホスト"
|
||||
smtpPort: "ポート"
|
||||
smtpUser: "ユーザー名"
|
||||
smtpPass: "パスワード"
|
||||
emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にすることで、SMTP認証を無効化出来ます"
|
||||
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
||||
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
||||
testEmail: "配信テスト"
|
||||
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
@ -725,6 +750,7 @@ _widgets:
|
||||
activity: "アクティビティ"
|
||||
photos: "フォト"
|
||||
digitalClock: "デジタル時計"
|
||||
federation: "連合"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
@ -1203,9 +1229,27 @@ _notification:
|
||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||
youWereInvitedToGroup: "グループに招待されました"
|
||||
|
||||
_types:
|
||||
all: "すべて"
|
||||
follow: "フォロー"
|
||||
mention: "メンション"
|
||||
reply: "リプライ"
|
||||
renote: "Renote"
|
||||
quote: "引用"
|
||||
reaction: "リアクション"
|
||||
pollVote: "投票"
|
||||
receiveFollowRequest: "フォローリクエスト"
|
||||
|
||||
_deck:
|
||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
addColumn: "カラムを追加"
|
||||
swapLeft: "左に移動"
|
||||
swapRight: "右に移動"
|
||||
swapUp: "上に移動"
|
||||
swapDown: "下に移動"
|
||||
stackLeft: "左に重ねる"
|
||||
popRight: "右に出す"
|
||||
|
||||
_columns:
|
||||
widgets: "ウィジェット"
|
||||
|
@ -295,6 +295,7 @@ proxyRemoteFilesDescription: "この設定を入れると、保存しとらん
|
||||
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
|
||||
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
|
||||
inMb: "メガバイト単位"
|
||||
recaptcha: "reCAPTCHA"
|
||||
avoidMultiCaptchaConfirm: "ぎょうさんのCaptchaをつこてしまうと、仲良うせんことがあるんや。他のCaptchaをなおしとこか?別にキャンセルしてもろうたらCaptchaは消されへんで済むけど知らんで。"
|
||||
antennas: "アンテナ"
|
||||
manageAntennas: "アンテナいじる"
|
||||
@ -352,6 +353,9 @@ notFoundDescription: "指定されたURLに該当するページはあらへん
|
||||
close: "さいなら"
|
||||
joinedGroups: "参加しとるグループ"
|
||||
invites: "来てや"
|
||||
smtpHost: "ホスト"
|
||||
smtpUser: "ユーザー名"
|
||||
smtpPass: "パスワード"
|
||||
_theme:
|
||||
keys:
|
||||
renote: "Renote"
|
||||
@ -386,6 +390,7 @@ _widgets:
|
||||
notifications: "通知"
|
||||
timeline: "タイムライン"
|
||||
activity: "アクティビティ"
|
||||
federation: "連合"
|
||||
_cw:
|
||||
show: "もっとあるやろ!"
|
||||
_poll:
|
||||
|
@ -33,6 +33,8 @@ youHaveNoLists: "Ulac ɣur-k·m ula d yiwet n tabdart"
|
||||
remove: "Kkes"
|
||||
userList: "Tibdarin"
|
||||
uiLanguage: "Tutlayt n wegrudem"
|
||||
smtpUser: "Isem n umseqdac"
|
||||
smtpPass: "Awal uffir"
|
||||
_theme:
|
||||
keys:
|
||||
mention: "Bder"
|
||||
|
@ -54,6 +54,8 @@ driveFileDeleteConfirm: "\"{name}\" ಕಡತವನ್ನು ಅಳಿಸಲು
|
||||
unfollowConfirm: "{name}ಅನ್ನು ಹಿಂಬಾಲಿಸದಿರುವುದೇ?"
|
||||
instances: "ನಿದರ್ಶನ"
|
||||
remove: "ಅಳಿಸು"
|
||||
smtpUser: "ಬಳಕೆಹೆಸರು"
|
||||
smtpPass: "ಗುಪ್ತಪದ"
|
||||
_sfx:
|
||||
notification: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
_widgets:
|
||||
|
@ -442,7 +442,6 @@ remote: "리모트"
|
||||
total: "합계"
|
||||
weekOverWeekChanges: "지난주보다"
|
||||
dayOverDayChanges: "어제보다"
|
||||
accessibility: "접근성"
|
||||
clinetSettings: "클라이언트 설정"
|
||||
accountSettings: "계정 설정"
|
||||
promotion: "프로모션"
|
||||
@ -511,9 +510,26 @@ addedRelays: "추가된 릴레이"
|
||||
serviceworkerInfo: "푸시 알림을 수행하려면 활성화해야 합니다."
|
||||
deletedNote: "삭제된 노트"
|
||||
invisibleNote: "비공개 노트"
|
||||
enableInfiniteScroll: "자동으로 좀 더 보기"
|
||||
visibility: "공개 범위"
|
||||
poll: "투표"
|
||||
useCw: "내용 숨기기"
|
||||
fixedWidgetsPosition: "위젯의 위치 고정"
|
||||
enablePlayer: "플레이어 열기"
|
||||
disablePlayer: "플레이어 닫기"
|
||||
expandTweet: "트윗 확장하기"
|
||||
themeEditor: "테마 에디터"
|
||||
description: "설명"
|
||||
author: "작성자"
|
||||
leaveConfirm: "저장하지 않은 변경사항이 있습니다. 취소하시겠습니까?"
|
||||
manage: "관리"
|
||||
plugins: "플러그인"
|
||||
pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오."
|
||||
deck: "덱"
|
||||
undeck: "덱 해제"
|
||||
smtpHost: "호스트"
|
||||
smtpUser: "유저명"
|
||||
smtpPass: "비밀번호"
|
||||
_theme:
|
||||
explore: "테마 찾아보기"
|
||||
install: "테마 설치"
|
||||
@ -522,8 +538,18 @@ _theme:
|
||||
installed: "{name} 테마가 설치되었습니다"
|
||||
alreadyInstalled: "이미 설치된 테마입니다"
|
||||
invalid: "테마 형식이 올바르지 않습니다"
|
||||
make: "테마 만들기"
|
||||
base: "베이스"
|
||||
addConstant: "상수 추가"
|
||||
constant: "상수"
|
||||
defaultValue: "기본값"
|
||||
color: "색"
|
||||
refProp: "프로퍼티를 참조"
|
||||
refConst: "상수를 참조"
|
||||
key: "키"
|
||||
func: "함수"
|
||||
funcKind: "함수 종류"
|
||||
argument: "매개변수"
|
||||
keys:
|
||||
mention: "멘션"
|
||||
renote: "Renote"
|
||||
@ -641,6 +667,8 @@ _widgets:
|
||||
rss: "RSS 리더"
|
||||
activity: "활동"
|
||||
photos: "사진"
|
||||
digitalClock: "디지털 시계"
|
||||
federation: "연합"
|
||||
_cw:
|
||||
hide: "숨기기"
|
||||
show: "더 보기"
|
||||
@ -1094,6 +1122,7 @@ _notification:
|
||||
youWereInvitedToGroup: "그룹에 초대되었습니다"
|
||||
_deck:
|
||||
_columns:
|
||||
widgets: "위젯"
|
||||
notifications: "알림"
|
||||
tl: "타임라인"
|
||||
antenna: "안테나"
|
||||
|
@ -31,6 +31,7 @@ importAndExport: "Импорт / Экспорт"
|
||||
files: "Файл"
|
||||
instances: "Экземпляр"
|
||||
remove: "Удалить"
|
||||
smtpPass: "Пароль"
|
||||
_sfx:
|
||||
notification: "Уведомления"
|
||||
_widgets:
|
||||
|
@ -46,7 +46,7 @@ youGotNewFollower: "你有新的关注者"
|
||||
receiveFollowRequest: "您收到了关注请求"
|
||||
followRequestAccepted: "您的关注请求被通过了"
|
||||
mention: "提及"
|
||||
mentions: "提到我的"
|
||||
mentions: "提及"
|
||||
directNotes: "私信"
|
||||
importAndExport: "导入和导出"
|
||||
import: "导入"
|
||||
@ -442,7 +442,7 @@ remote: "远程"
|
||||
total: "总计"
|
||||
weekOverWeekChanges: "与前一周相比"
|
||||
dayOverDayChanges: "与前一日相比"
|
||||
accessibility: "辅助功能"
|
||||
appearance: "外观"
|
||||
clinetSettings: "客户端设置"
|
||||
accountSettings: "账户设置"
|
||||
promotion: "推广"
|
||||
@ -523,8 +523,22 @@ themeEditor: "主题编辑器"
|
||||
description: "描述"
|
||||
author: "作者"
|
||||
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
|
||||
manage: "管理"
|
||||
plugins: "插件"
|
||||
pluginInstallWarn: "请不要安装不明来源的插件"
|
||||
deck: "Deck"
|
||||
undeck: "取消Deck"
|
||||
useBlurEffectForModal: "模态框使用模糊效果"
|
||||
generateAccessToken: "生成访问令牌"
|
||||
permission: "权限"
|
||||
enableAll: "启用全部"
|
||||
disableAll: "禁用全部"
|
||||
tokenRequested: "允许访问账户"
|
||||
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
|
||||
smtpHost: "主机名"
|
||||
smtpPort: "端口"
|
||||
smtpUser: "用户名"
|
||||
smtpPass: "密码"
|
||||
_theme:
|
||||
explore: "寻找主题"
|
||||
install: "安装主题"
|
||||
@ -533,18 +547,69 @@ _theme:
|
||||
installed: "{name} 已安装"
|
||||
alreadyInstalled: "此主题已经安装"
|
||||
invalid: "主题格式错误"
|
||||
make: "主题制作"
|
||||
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: "聊天背景"
|
||||
accentDarken: "强调色(暗)"
|
||||
accentLighten: "强调色(亮)"
|
||||
fgHighlighted: "高亮显示文本"
|
||||
_sfx:
|
||||
note: "帖子"
|
||||
noteMy: "我的帖子"
|
||||
@ -659,6 +724,7 @@ _widgets:
|
||||
activity: "活动"
|
||||
photos: "照片"
|
||||
digitalClock: "数字时钟"
|
||||
federation: "联邦宇宙"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
@ -1115,11 +1181,12 @@ _notification:
|
||||
_deck:
|
||||
alwaysShowMainColumn: "总是显示主列"
|
||||
columnAlign: "列对齐"
|
||||
addColumn: "添加列"
|
||||
_columns:
|
||||
widgets: "小部件"
|
||||
notifications: "通知"
|
||||
tl: "时间线"
|
||||
antenna: "天线"
|
||||
list: "列表"
|
||||
mentions: "提到我的"
|
||||
mentions: "提及"
|
||||
direct: "指定用户"
|
||||
|
@ -4,13 +4,13 @@ introMisskey: "歡迎! Misskey是一個開源的去中心化的社群網站。
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "搜尋"
|
||||
notifications: "通知"
|
||||
username: "用戶名"
|
||||
username: "使用名稱"
|
||||
password: "密碼"
|
||||
fetchingAsApObject: "從Fediverse尋找中..."
|
||||
ok: "OK"
|
||||
fetchingAsApObject: "從 Fediverse 查詢中..."
|
||||
ok: "確定"
|
||||
gotIt: "知道了"
|
||||
cancel: "取消"
|
||||
enterUsername: "輸入用戶名"
|
||||
enterUsername: "輸入使用者名稱"
|
||||
renotedBy: "由{user}轉發"
|
||||
noNotes: "貼文不可用。"
|
||||
noNotifications: "沒有通知"
|
||||
@ -24,11 +24,11 @@ loggingIn: "登入中"
|
||||
logout: "登出"
|
||||
signup: "註冊"
|
||||
uploading: "上傳中"
|
||||
save: "保存"
|
||||
users: "用戶"
|
||||
addUser: "新增用戶"
|
||||
save: "儲存"
|
||||
users: "使用者"
|
||||
addUser: "新增使用者"
|
||||
favorite: "收藏"
|
||||
favorites: "收藏"
|
||||
favorites: "已加星號"
|
||||
unfavorite: "取消收藏"
|
||||
pin: "置頂"
|
||||
unpin: "取消置頂"
|
||||
@ -82,7 +82,7 @@ unrenote: "取消轉發貼文"
|
||||
quote: "引用"
|
||||
pinnedNote: "已置頂的貼文"
|
||||
you: "您"
|
||||
clickToShow: "點擊查看"
|
||||
clickToShow: "按一下以顯示"
|
||||
sensitive: "敏感內容"
|
||||
add: "新增"
|
||||
reaction: "反應"
|
||||
@ -92,8 +92,8 @@ attachCancel: "移除附件"
|
||||
markAsSensitive: "標記為敏感內容"
|
||||
unmarkAsSensitive: "取消標記為敏感內容"
|
||||
enterFileName: "請輸入檔案名稱"
|
||||
mute: "禁言"
|
||||
unmute: "解除禁言"
|
||||
mute: "消音"
|
||||
unmute: "解除消音"
|
||||
block: "封鎖"
|
||||
unblock: "解除封鎖"
|
||||
suspend: "凍結"
|
||||
@ -108,42 +108,48 @@ emoji: "表情符號"
|
||||
emojiName: "表情符號名稱"
|
||||
emojiUrl: "表情符號URL"
|
||||
addEmoji: "新增表情符號"
|
||||
settingGuide: "推介設定"
|
||||
settingGuide: "推薦設定"
|
||||
flagAsBot: "此帳戶是Bot"
|
||||
flagAsCat: "此帳戶是Cat"
|
||||
autoAcceptFollowed: "自動許可追隨"
|
||||
addAcount: "新增帳戶"
|
||||
addAcount: "新增帳號"
|
||||
loginFailed: "登入失敗"
|
||||
general: "一般"
|
||||
wallpaper: "壁紙"
|
||||
wallpaper: "桌布"
|
||||
setWallpaper: "設定桌布"
|
||||
removeWallpaper: "移除壁紙"
|
||||
removeWallpaper: "移除桌布"
|
||||
searchWith: "搜尋: {q}"
|
||||
youHaveNoLists: "你沒有任何清單"
|
||||
followConfirm: "你真的要追隨{name}嗎?"
|
||||
youHaveNoLists: "沒有任何清單"
|
||||
followConfirm: "你真的要關注{name}嗎?"
|
||||
proxyAccount: "代理帳號"
|
||||
host: "主機"
|
||||
selectUser: "選擇用戶"
|
||||
recipient: "收件人"
|
||||
selectUser: "選取使用者"
|
||||
recipient: "發送至"
|
||||
annotation: "註解"
|
||||
federation: "整合"
|
||||
federation: "聯邦宇宙"
|
||||
instances: "實例"
|
||||
latestStatus: "最後狀態"
|
||||
storageUsage: "已使用容量"
|
||||
charts: "圖表"
|
||||
perHour: "每小時"
|
||||
perDay: "每日"
|
||||
blockThisInstance: "封鎖此實例"
|
||||
operations: "操作"
|
||||
software: "軟體"
|
||||
version: "版本"
|
||||
metadata: "元資料(Metadata)"
|
||||
withNFiles: "{n}個檔案"
|
||||
monitor: "監視器"
|
||||
jobQueue: "佇列"
|
||||
cpuAndMemory: "CPU及記憶體用量"
|
||||
network: "網路"
|
||||
disk: "硬碟"
|
||||
instanceInfo: "實例資訊"
|
||||
statistics: "統計"
|
||||
clearQueue: "清除佇列"
|
||||
clearQueueConfirmTitle: "確定要清除佇列嗎?"
|
||||
clearCachedFiles: "清除快取資料"
|
||||
clearCachedFilesConfirm: "確定要清除緩存資料嗎?"
|
||||
blockedInstances: "已封鎖的實例"
|
||||
blockedInstancesDescription: "請逐行輸入需要封鎖的實例。已封鎖的實例將無法與本實例進行通訊。"
|
||||
muteAndBlock: "禁言 / 封鎖"
|
||||
@ -153,14 +159,15 @@ noUsers: "無用戶"
|
||||
editProfile: "編輯個人檔案"
|
||||
noteDeleteConfirm: "確定刪除此貼文嗎?"
|
||||
pinLimitExceeded: "不能再置頂更多的貼文了"
|
||||
intro: "Misskey安裝作業完成!請創立管理員用戶"
|
||||
intro: "Misskey 部署完成!請開設管理員帳號!"
|
||||
done: "完成"
|
||||
processing: "處理中"
|
||||
preview: "預覽"
|
||||
default: "預設"
|
||||
noCustomEmojis: "沒有表情符號"
|
||||
customEmojisOfRemote: "來自其他實例的表情符號"
|
||||
federating: "整合檢索中"
|
||||
noJobs: "沒有任務"
|
||||
federating: "整合搜索中"
|
||||
blocked: "已封鎖"
|
||||
suspended: "已凍結"
|
||||
all: "全部"
|
||||
@ -175,12 +182,13 @@ security: "安全性"
|
||||
retypedNotMatch: "不相符的輸入內容"
|
||||
currentPassword: "現在的密碼"
|
||||
newPassword: "新的密碼"
|
||||
newPasswordRetype: "新的密碼(再輸入一次)"
|
||||
newPasswordRetype: "新的密碼(再輸入一次)"
|
||||
attachFile: "添加附件"
|
||||
more: "更多!"
|
||||
featured: "精選"
|
||||
usernameOrUserId: "用戶名或用戶ID"
|
||||
noSuchUser: "用戶不存在"
|
||||
usernameOrUserId: "使用者名稱或使用者 ID"
|
||||
noSuchUser: "使用者不存在"
|
||||
lookup: "查詢"
|
||||
announcements: "公告"
|
||||
imageUrl: "圖片URL"
|
||||
remove: "刪除"
|
||||
@ -373,6 +381,7 @@ passwordMatched: "密碼一致"
|
||||
passwordNotMatched: "密碼不一致"
|
||||
signinFailed: "登入失敗。 請檢查用戶名和密碼。"
|
||||
uiLanguage: "介面語言"
|
||||
youHaveNoGroups: "找不到群組"
|
||||
tags: "標籤"
|
||||
fontSize: "字體大小"
|
||||
total: "合計"
|
||||
@ -389,11 +398,17 @@ install: "安裝"
|
||||
uninstall: "解除安裝"
|
||||
lastUsedDate: "最後上線日期"
|
||||
state: "狀態"
|
||||
ascendingOrder: "昇冪"
|
||||
descendingOrder: "降冪"
|
||||
scratchpad: "暫存記憶體"
|
||||
output: "輸出"
|
||||
deleteAllFiles: "刪除所有檔案"
|
||||
deleteAllFilesConfirm: "要删除所有檔案吗?"
|
||||
userSilenced: "該用戶已被禁言。"
|
||||
deletedNote: "已删除的貼文"
|
||||
smtpHost: "主機"
|
||||
smtpUser: "使用名稱"
|
||||
smtpPass: "密碼"
|
||||
_theme:
|
||||
func: "函数"
|
||||
keys:
|
||||
@ -469,6 +484,7 @@ _widgets:
|
||||
rss: "RSS閱讀器"
|
||||
activity: "動態"
|
||||
photos: "照片"
|
||||
federation: "聯邦宇宙"
|
||||
_cw:
|
||||
show: "瀏覽更多"
|
||||
files: "{count} 個檔案"
|
||||
@ -481,10 +497,10 @@ _visibility:
|
||||
followers: "追隨者"
|
||||
_profile:
|
||||
name: "名稱"
|
||||
username: "用戶名"
|
||||
username: "使用名稱"
|
||||
_exportOrImport:
|
||||
followingList: "追隨中"
|
||||
muteList: "禁言"
|
||||
muteList: "消音"
|
||||
blockingList: "封鎖"
|
||||
userLists: "清單"
|
||||
_instanceCharts:
|
||||
|
14
migration/1595075960584-blurhash.ts
Normal file
14
migration/1595075960584-blurhash.ts
Normal 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"`);
|
||||
}
|
||||
|
||||
}
|
20
migration/1595077605646-blurhash-for-avatar-banner.ts
Normal file
20
migration/1595077605646-blurhash-for-avatar-banner.ts
Normal 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)`);
|
||||
}
|
||||
|
||||
}
|
14
migration/1595676934834-instance-icon-url.ts
Normal file
14
migration/1595676934834-instance-icon-url.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class instanceIconUrl1595676934834 implements MigrationInterface {
|
||||
name = 'instanceIconUrl1595676934834'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "instance" ADD "iconUrl" character varying(256) DEFAULT null`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "iconUrl"`);
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.41.1",
|
||||
"version": "12.43.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",
|
||||
|
@ -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;
|
||||
|
||||
|
BIN
src/client/assets/sounds/syuilo/pirori-square-wet.mp3
Normal file
BIN
src/client/assets/sounds/syuilo/pirori-square-wet.mp3
Normal file
Binary file not shown.
BIN
src/client/assets/sounds/syuilo/pirori-wet.mp3
Normal file
BIN
src/client/assets/sounds/syuilo/pirori-wet.mp3
Normal file
Binary file not shown.
@ -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,24 +39,25 @@ 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;
|
||||
'user.avatarBlurhash'() {
|
||||
this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.user.avatarColor) {
|
||||
this.$el.style.color = this.user.avatarColor;
|
||||
}
|
||||
this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
|
||||
},
|
||||
methods: {
|
||||
getBlurhashAvgColor(s) {
|
||||
return typeof s == 'string'
|
||||
? '#' + [...s.slice(2, 6)]
|
||||
.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
|
||||
.reduce((a, c) => a * 83 + c, 0)
|
||||
.toString(16)
|
||||
.padStart(6, '0')
|
||||
: undefined;
|
||||
},
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
}
|
||||
@ -102,15 +97,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>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
|
||||
<x-timeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
@ -33,7 +33,6 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
faSatellite
|
||||
};
|
||||
},
|
||||
@ -47,28 +46,36 @@ export default Vue.extend({
|
||||
created() {
|
||||
this.menu = [{
|
||||
icon: faCog,
|
||||
text: this.$t('antenna'),
|
||||
action: async () => {
|
||||
text: this.$t('selectAntenna'),
|
||||
action: this.setAntenna
|
||||
}];
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.column.antennaId == null) {
|
||||
this.setAntenna();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setAntenna() {
|
||||
const antennas = await this.$root.api('antennas/list');
|
||||
this.$root.dialog({
|
||||
title: this.$t('antenna'),
|
||||
const { canceled, result: antenna } = await this.$root.dialog({
|
||||
title: this.$t('selectAntenna'),
|
||||
type: null,
|
||||
select: {
|
||||
items: antennas.map(x => ({
|
||||
value: x, text: x.name
|
||||
}))
|
||||
})),
|
||||
default: this.column.antennaId
|
||||
},
|
||||
showCancelButton: true
|
||||
}).then(({ canceled, result: antenna }) => {
|
||||
if (canceled) return;
|
||||
this.column.antennaId = antenna.id;
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
});
|
||||
}
|
||||
}];
|
||||
if (canceled) return;
|
||||
Vue.set(this.column, 'antennaId', antenna.id);
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
}
|
||||
|
@ -150,37 +150,37 @@ export default Vue.extend({
|
||||
}
|
||||
}, null, {
|
||||
icon: faArrowLeft,
|
||||
text: this.$t('swap-left'),
|
||||
text: this.$t('_deck.swapLeft'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id);
|
||||
}
|
||||
}, {
|
||||
icon: faArrowRight,
|
||||
text: this.$t('swap-right'),
|
||||
text: this.$t('_deck.swapRight'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id);
|
||||
}
|
||||
}, this.isStacked ? {
|
||||
icon: faArrowUp,
|
||||
text: this.$t('swap-up'),
|
||||
text: this.$t('_deck.swapUp'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id);
|
||||
}
|
||||
} : undefined, this.isStacked ? {
|
||||
icon: faArrowDown,
|
||||
text: this.$t('swap-down'),
|
||||
text: this.$t('_deck.swapDown'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id);
|
||||
}
|
||||
} : undefined, null, {
|
||||
icon: faWindowRestore,
|
||||
text: this.$t('stack-left'),
|
||||
text: this.$t('_deck.stackLeft'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id);
|
||||
}
|
||||
}, this.isStacked ? {
|
||||
icon: faWindowMaximize,
|
||||
text: this.$t('pop-right'),
|
||||
text: this.$t('_deck.popRight'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/popRightDeckColumn', this.column.id);
|
||||
}
|
||||
|
@ -2,20 +2,21 @@
|
||||
<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-direct/>
|
||||
<x-notes :pagination="pagination" @before="before()" @after="after()"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
||||
import Progress from '../../scripts/loading';
|
||||
import XColumn from './column.vue';
|
||||
import XDirect from '../../pages/messages.vue';
|
||||
import XNotes from '../notes.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XDirect
|
||||
XNotes
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -32,8 +33,25 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
pagination: {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
visibility: 'specified'
|
||||
})
|
||||
},
|
||||
faEnvelope
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
before() {
|
||||
Progress.start();
|
||||
},
|
||||
|
||||
after() {
|
||||
Progress.done();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -46,7 +46,7 @@ export default Vue.extend({
|
||||
created() {
|
||||
this.menu = [{
|
||||
icon: faCog,
|
||||
text: this.$t('list'),
|
||||
text: this.$t('selectList'),
|
||||
action: this.setList
|
||||
}];
|
||||
},
|
||||
@ -61,7 +61,7 @@ export default Vue.extend({
|
||||
async setList() {
|
||||
const lists = await this.$root.api('users/lists/list');
|
||||
const { canceled, result: list } = await this.$root.dialog({
|
||||
title: this.$t('list'),
|
||||
title: this.$t('selectList'),
|
||||
type: null,
|
||||
select: {
|
||||
items: lists.map(x => ({
|
||||
|
@ -2,20 +2,21 @@
|
||||
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-mentions/>
|
||||
<x-notes :pagination="pagination" @before="before()" @after="after()"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faAt } from '@fortawesome/free-solid-svg-icons';
|
||||
import Progress from '../../scripts/loading';
|
||||
import XColumn from './column.vue';
|
||||
import XMentions from '../../pages/mentions.vue';
|
||||
import XNotes from '../notes.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XMentions
|
||||
XNotes
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -32,8 +33,22 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
pagination: {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
},
|
||||
faAt
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
before() {
|
||||
Progress.start();
|
||||
},
|
||||
|
||||
after() {
|
||||
Progress.done();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -45,14 +45,14 @@ export default Vue.extend({
|
||||
|
||||
this.menu = [{
|
||||
icon: faCog,
|
||||
text: this.$t('@.notification-type'),
|
||||
text: this.$t('notificationType'),
|
||||
action: () => {
|
||||
this.$root.dialog({
|
||||
title: this.$t('@.notification-type'),
|
||||
title: this.$t('notificationType'),
|
||||
type: null,
|
||||
select: {
|
||||
items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
|
||||
value: x, text: this.$t('@.notification-types.' + x)
|
||||
value: x, text: this.$t(`_notification._types.${x}`)
|
||||
}))
|
||||
default: this.column.notificationType,
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -5,9 +5,12 @@
|
||||
<div class="wtdtxvec">
|
||||
<template v-if="edit">
|
||||
<header>
|
||||
<select v-model="widgetAdderSelected" @change="addWidget">
|
||||
<option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option>
|
||||
</select>
|
||||
<mk-select v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
|
||||
<template #label>{{ $t('selectWidget') }}</template>
|
||||
<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
|
||||
</mk-select>
|
||||
<mk-button inline @click="addWidget" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
|
||||
<mk-button inline @click="edit = false">{{ $t('close') }}</mk-button>
|
||||
</header>
|
||||
<x-draggable
|
||||
:list="column.widgets"
|
||||
@ -15,7 +18,7 @@
|
||||
@sort="onWidgetSort"
|
||||
>
|
||||
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
|
||||
<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
|
||||
<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><fa :icon="faTimes"/></button>
|
||||
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
|
||||
</div>
|
||||
</x-draggable>
|
||||
@ -29,7 +32,9 @@
|
||||
import Vue from 'vue';
|
||||
import * as XDraggable from 'vuedraggable';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { faWindowMaximize, faTimes, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import XColumn from './column.vue';
|
||||
import { widgets } from '../../widgets';
|
||||
|
||||
@ -37,6 +42,8 @@ export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XDraggable,
|
||||
MkSelect,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -56,7 +63,7 @@ export default Vue.extend({
|
||||
menu: null,
|
||||
widgetAdderSelected: null,
|
||||
widgets,
|
||||
faWindowMaximize, faTimes
|
||||
faWindowMaximize, faTimes, faPlus
|
||||
};
|
||||
},
|
||||
|
||||
@ -80,6 +87,8 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
addWidget() {
|
||||
if (this.widgetAdderSelected == null) return;
|
||||
|
||||
this.$store.commit('deviceUser/addDeckWidget', {
|
||||
id: this.column.id,
|
||||
widget: {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
pointer-events: none;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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 {
|
||||
|
78
src/client/components/img-with-blurhash.vue
Normal file
78
src/client/components/img-with-blurhash.vue
Normal 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>
|
@ -1,19 +1,22 @@
|
||||
<template>
|
||||
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="hide" @click="hide = false">
|
||||
<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 class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else>
|
||||
</div>
|
||||
<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
|
||||
url(): any {
|
||||
let url = this.$store.state.device.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(this.image.thumbnailUrl)
|
||||
: 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>
|
||||
|
@ -114,7 +114,7 @@ export default Vue.extend({
|
||||
|
||||
> * {
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&[data-count="1"] {
|
||||
|
@ -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 {
|
||||
|
@ -34,19 +34,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<article class="article">
|
||||
<mk-avatar class="avatar" :user="appearNote.user"/>
|
||||
<mk-avatar class="avatar" :user="appearNote.user" v-once/>
|
||||
<div class="main">
|
||||
<x-note-header class="header" :note="appearNote" :mini="true"/>
|
||||
<div class="body" v-if="appearNote.deletedAt == null" 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" />
|
||||
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
|
||||
<x-cw-button v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
|
||||
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
|
||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||
</div>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
|
@ -52,9 +52,9 @@ export default Vue.extend({
|
||||
},
|
||||
timer(): string {
|
||||
return this.$t(
|
||||
this.remaining > 86400 ? '_poll.remainingDays' :
|
||||
this.remaining > 3600 ? '_poll.remainingHours' :
|
||||
this.remaining > 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
|
||||
this.remaining >= 86400 ? '_poll.remainingDays' :
|
||||
this.remaining >= 3600 ? '_poll.remainingHours' :
|
||||
this.remaining >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
|
||||
s: Math.floor(this.remaining % 60),
|
||||
m: Math.floor(this.remaining / 60) % 60,
|
||||
h: Math.floor(this.remaining / 3600) % 24,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -61,6 +61,7 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
tick() {
|
||||
// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
|
||||
this.now = new Date();
|
||||
|
||||
this.tickId = setTimeout(() => {
|
||||
|
115
src/client/components/token-generate-window.vue
Normal file
115
src/client/components/token-generate-window.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
const os = new MiOS(store);
|
||||
|
||||
os.init(async () => {
|
||||
// 他のタブと永続化されたstateを同期
|
||||
window.addEventListener('storage', e => {
|
||||
if (e.key === 'vuex') {
|
||||
store.replaceState(JSON.parse(localStorage['vuex']));
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
...JSON.parse(e.newValue)
|
||||
});
|
||||
} 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
|
||||
const os = new MiOS(store);
|
||||
|
||||
os.init(async () => {
|
||||
//#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');
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
|
@ -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;"/>
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true">
|
||||
<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true" :width="520" :height="500">
|
||||
<template #header>{{ instance.host }}</template>
|
||||
<div class="mk-instance-info">
|
||||
<div class="table info">
|
||||
|
@ -28,6 +28,9 @@
|
||||
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
|
||||
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="useStarForReactionFallback" @change="save()">{{ $t('useStarForReactionFallback') }}</mk-switch>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
@ -74,6 +77,29 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch>
|
||||
<mk-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</mk-input>
|
||||
<div><b>{{ $t('smtpConfig') }}</b></div>
|
||||
<div class="_inputs">
|
||||
<mk-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</mk-input>
|
||||
<mk-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</mk-input>
|
||||
</div>
|
||||
<div class="_inputs">
|
||||
<mk-input v-model="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</mk-input>
|
||||
<mk-input v-model="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</mk-input>
|
||||
</div>
|
||||
<mk-info>{{ $t('emptyToDisableSmtpAuth') }}</mk-info>
|
||||
<mk-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></mk-switch>
|
||||
<div>
|
||||
<mk-button :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</mk-button>
|
||||
<mk-button :disabled="!enableEmail" primary inline @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
|
||||
<div class="_content">
|
||||
@ -195,12 +221,19 @@
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="summalyProxy">URL</mk-input>
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
@ -243,7 +276,9 @@ export default Vue.extend({
|
||||
maintainerEmail: null,
|
||||
name: null,
|
||||
description: null,
|
||||
tosUrl: null,
|
||||
tosUrl: null as string | null,
|
||||
enableEmail: false,
|
||||
email: null,
|
||||
bannerUrl: null,
|
||||
iconUrl: null,
|
||||
maxNoteTextLength: 0,
|
||||
@ -279,7 +314,14 @@ export default Vue.extend({
|
||||
enableDiscordIntegration: false,
|
||||
discordClientId: null,
|
||||
discordClientSecret: null,
|
||||
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
|
||||
useStarForReactionFallback: false,
|
||||
smtpSecure: false,
|
||||
smtpHost: '',
|
||||
smtpPort: 0,
|
||||
smtpUser: '',
|
||||
smtpPass: '',
|
||||
summalyProxy: '',
|
||||
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway
|
||||
}
|
||||
},
|
||||
|
||||
@ -295,6 +337,8 @@ export default Vue.extend({
|
||||
this.tosUrl = this.meta.tosUrl;
|
||||
this.bannerUrl = this.meta.bannerUrl;
|
||||
this.iconUrl = this.meta.iconUrl;
|
||||
this.enableEmail = this.meta.enableEmail;
|
||||
this.email = this.meta.email;
|
||||
this.maintainerName = this.meta.maintainerName;
|
||||
this.maintainerEmail = this.meta.maintainerEmail;
|
||||
this.maxNoteTextLength = this.meta.maxNoteTextLength;
|
||||
@ -337,6 +381,13 @@ export default Vue.extend({
|
||||
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
|
||||
this.discordClientId = this.meta.discordClientId;
|
||||
this.discordClientSecret = this.meta.discordClientSecret;
|
||||
this.useStarForReactionFallback = this.meta.useStarForReactionFallback;
|
||||
this.smtpSecure = this.meta.smtpSecure;
|
||||
this.smtpHost = this.meta.smtpHost;
|
||||
this.smtpPort = this.meta.smtpPort;
|
||||
this.smtpUser = this.meta.smtpUser;
|
||||
this.smtpPass = this.meta.smtpPass;
|
||||
this.summalyProxy = this.meta.summalyProxy;
|
||||
|
||||
if (this.proxyAccountId) {
|
||||
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
|
||||
@ -412,6 +463,24 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
async testEmail() {
|
||||
this.$root.api('admin/send-email', {
|
||||
to: this.maintainerEmail,
|
||||
subject: 'Test email',
|
||||
text: 'Yo'
|
||||
}).then(x => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
splash: true
|
||||
});
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
save(withDialog = false) {
|
||||
this.$root.api('admin/update-meta', {
|
||||
name: this.name,
|
||||
@ -461,6 +530,15 @@ export default Vue.extend({
|
||||
enableDiscordIntegration: this.enableDiscordIntegration,
|
||||
discordClientId: this.discordClientId,
|
||||
discordClientSecret: this.discordClientSecret,
|
||||
enableEmail: this.enableEmail,
|
||||
email: this.email,
|
||||
smtpSecure: this.smtpSecure,
|
||||
smtpHost: this.smtpHost,
|
||||
smtpPort: this.smtpPort,
|
||||
smtpUser: this.smtpUser,
|
||||
smtpPass: this.smtpPass,
|
||||
summalyProxy: this.summalyProxy,
|
||||
useStarForReactionFallback: this.useStarForReactionFallback,
|
||||
}).then(() => {
|
||||
this.$store.dispatch('instance/fetch');
|
||||
if (withDialog) {
|
||||
|
@ -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]) {
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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'),
|
||||
|
@ -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>
|
||||
|
@ -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 }); }
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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,13 @@ export function createAiScriptEnv(vm, opts) {
|
||||
}
|
||||
|
||||
export function createPluginEnv(vm, opts) {
|
||||
const config = new Map();
|
||||
for (const [k, v] of Object.entries(opts.plugin.config)) {
|
||||
config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
|
||||
}
|
||||
|
||||
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 +59,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),
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)',
|
||||
|
@ -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)',
|
||||
|
@ -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',
|
||||
},
|
||||
|
126
src/client/widgets/federation.vue
Normal file
126
src/client/widgets/federation.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<mk-container :show-header="props.showHeader">
|
||||
<template #header><fa :icon="faGlobe"/>{{ $t('_widgets.federation') }}</template>
|
||||
|
||||
<div class="wbrkwalb">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<transition-group tag="div" name="chart" class="instances" v-else>
|
||||
<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
|
||||
<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
|
||||
<div class="body">
|
||||
<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
|
||||
<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
|
||||
</div>
|
||||
<mk-mini-chart class="chart" :src="charts[i].requests.received"/>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { faGlobe } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkContainer from '../components/ui/container.vue';
|
||||
import define from './define';
|
||||
import MkMiniChart from '../components/mini-chart.vue';
|
||||
|
||||
export default define({
|
||||
name: 'federation',
|
||||
props: () => ({
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
MkContainer, MkMiniChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
instances: [],
|
||||
charts: [],
|
||||
fetching: true,
|
||||
faGlobe
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
this.clock = setInterval(this.fetch, 1000 * 60);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
async fetch() {
|
||||
const instances = await this.$root.api('federation/instances', {
|
||||
sort: '+lastCommunicatedAt',
|
||||
limit: 5
|
||||
});
|
||||
const charts = await Promise.all(instances.map(i => this.$root.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
|
||||
this.instances = instances;
|
||||
this.charts = charts;
|
||||
this.fetching = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wbrkwalb {
|
||||
$bodyTitleHieght: 18px;
|
||||
$bodyInfoHieght: 16px;
|
||||
|
||||
height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
|
||||
overflow: hidden;
|
||||
|
||||
> .instances {
|
||||
.chart-move {
|
||||
transition: transform 1s ease;
|
||||
}
|
||||
|
||||
> .instance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border-bottom: solid 1px var(--divider);
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: ($bodyTitleHieght + $bodyInfoHieght);
|
||||
height: ($bodyTitleHieght + $bodyInfoHieght);
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 0.9em;
|
||||
color: var(--fg);
|
||||
|
||||
> .a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: $bodyTitleHieght;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 75%;
|
||||
opacity: 0.7;
|
||||
line-height: $bodyInfoHieght;
|
||||
}
|
||||
}
|
||||
|
||||
> .chart {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -11,6 +11,7 @@ Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
|
||||
Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));
|
||||
Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default));
|
||||
Vue.component('mkw-digitalClock', () => import('./digital-clock.vue').then(m => m.default));
|
||||
Vue.component('mkw-federation', () => import('./federation.vue').then(m => m.default));
|
||||
|
||||
export const widgets = [
|
||||
'memo',
|
||||
@ -23,4 +24,5 @@ export const widgets = [
|
||||
'activity',
|
||||
'photos',
|
||||
'digitalClock',
|
||||
'federation',
|
||||
];
|
||||
|
@ -10,7 +10,7 @@
|
||||
<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
|
||||
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
</div>
|
||||
<x-chart class="chart" :src="stat.chart"/>
|
||||
<mk-mini-chart class="chart" :src="stat.chart"/>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
@ -21,7 +21,7 @@
|
||||
import { faHashtag } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkContainer from '../components/ui/container.vue';
|
||||
import define from './define';
|
||||
import XChart from './trends.chart.vue';
|
||||
import MkMiniChart from '../components/mini-chart.vue';
|
||||
|
||||
export default define({
|
||||
name: 'hashtags',
|
||||
@ -33,7 +33,7 @@ export default define({
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
MkContainer, XChart
|
||||
MkContainer, MkMiniChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -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` ... ユーザーのアクセストークン
|
||||
|
@ -21,8 +21,8 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
|
||||
return lock(`ap-object:${uri}`, timeout);
|
||||
}
|
||||
|
||||
export function getNodeinfoLock(host: string, timeout = 30 * 1000) {
|
||||
return lock(`nodeinfo:${host}`, timeout);
|
||||
export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) {
|
||||
return lock(`instance:${host}`, timeout);
|
||||
}
|
||||
|
||||
export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {
|
||||
|
@ -27,6 +27,27 @@ export async function getJson(url: string, accept = 'application/json, */*', tim
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: HeadersInit) {
|
||||
const res = await fetch(url, {
|
||||
headers: Object.assign({
|
||||
'User-Agent': config.userAgent,
|
||||
Accept: accept
|
||||
}, headers || {}),
|
||||
timeout,
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw {
|
||||
name: `StatusError`,
|
||||
statusCode: res.status,
|
||||
message: `${res.status} ${res.statusText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get http non-proxy agent
|
||||
*/
|
||||
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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.'
|
||||
|
@ -158,6 +158,11 @@ export class Instance {
|
||||
})
|
||||
public maintainerEmail: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, nullable: true, default: null,
|
||||
})
|
||||
public iconUrl: string | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -4,7 +4,7 @@ import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-ins
|
||||
import Logger from '../../services/logger';
|
||||
import { Instances } from '../../models';
|
||||
import { instanceChart } from '../../services/chart';
|
||||
import { fetchNodeinfo } from '../../services/fetch-nodeinfo';
|
||||
import { fetchInstanceMetadata } from '../../services/fetch-instance-metadata';
|
||||
import { fetchMeta } from '../../misc/fetch-meta';
|
||||
import { toPuny } from '../../misc/convert-host';
|
||||
|
||||
@ -48,7 +48,7 @@ export default async (job: Bull.Job) => {
|
||||
isNotResponding: false
|
||||
});
|
||||
|
||||
fetchNodeinfo(i);
|
||||
fetchInstanceMetadata(i);
|
||||
|
||||
instanceChart.requestSent(i.host, true);
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ import { instanceChart } from '../../services/chart';
|
||||
import { fetchMeta } from '../../misc/fetch-meta';
|
||||
import { toPuny, extractDbHost } from '../../misc/convert-host';
|
||||
import { getApId } from '../../remote/activitypub/type';
|
||||
import { fetchNodeinfo } from '../../services/fetch-nodeinfo';
|
||||
import { fetchInstanceMetadata } from '../../services/fetch-instance-metadata';
|
||||
import { InboxJobData } from '..';
|
||||
import DbResolver from '../../remote/activitypub/db-resolver';
|
||||
import { resolvePerson } from '../../remote/activitypub/models/person';
|
||||
@ -47,7 +47,15 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
|
||||
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
|
||||
if (authUser == null) {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
// それでもわからなければ終了
|
||||
@ -118,7 +126,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
isNotResponding: false
|
||||
});
|
||||
|
||||
fetchNodeinfo(i);
|
||||
fetchInstanceMetadata(i);
|
||||
|
||||
instanceChart.requestReceived(i.host);
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ import { validActor } from '../../../remote/activitypub/type';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { ensure } from '../../../prelude/ensure';
|
||||
import { toArray } from '../../../prelude/array';
|
||||
import { fetchNodeinfo } from '../../../services/fetch-nodeinfo';
|
||||
import { fetchInstanceMetadata } from '../../../services/fetch-instance-metadata';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
@ -204,7 +204,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
registerOrFetchInstanceDoc(host).then(i => {
|
||||
Instances.increment({ id: i.id }, 'usersCount', 1);
|
||||
instanceChart.newUser(i.host);
|
||||
fetchNodeinfo(i);
|
||||
fetchInstanceMetadata(i);
|
||||
});
|
||||
|
||||
usersChart.update(user!, true);
|
||||
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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[];
|
||||
|
@ -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
|
||||
|
@ -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 :
|
||||
|
135
src/services/fetch-instance-metadata.ts
Normal file
135
src/services/fetch-instance-metadata.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import fetch from 'node-fetch';
|
||||
import { getJson, getHtml, getAgentByUrl } from '../misc/fetch';
|
||||
import { Instance } from '../models/entities/instance';
|
||||
import { Instances } from '../models';
|
||||
import { getFetchInstanceMetadataLock } from '../misc/app-lock';
|
||||
import Logger from './logger';
|
||||
import { URL } from 'url';
|
||||
|
||||
const logger = new Logger('metadata', 'cyan');
|
||||
|
||||
export async function fetchInstanceMetadata(instance: Instance): 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;
|
||||
}
|
||||
|
||||
logger.info(`Fetching metadata of ${instance.host} ...`);
|
||||
|
||||
try {
|
||||
const [info, icon] = await Promise.all([
|
||||
fetchNodeinfo(instance).catch(() => null),
|
||||
fetchIconUrl(instance).catch(() => null),
|
||||
]);
|
||||
|
||||
logger.succ(`Successfuly fetched metadata of ${instance.host}`);
|
||||
|
||||
const updates = {
|
||||
infoUpdatedAt: new Date(),
|
||||
} as Record<string, any>;
|
||||
|
||||
if (info) {
|
||||
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;
|
||||
}
|
||||
|
||||
await Instances.update(instance.id, updates);
|
||||
|
||||
logger.succ(`Successfuly updated metadata of ${instance.host}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> {
|
||||
logger.info(`Fetching nodeinfo of ${instance.host} ...`);
|
||||
|
||||
try {
|
||||
const wellknown = await getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
||||
.catch(e => {
|
||||
if (e.statusCode === 404) {
|
||||
throw 'No nodeinfo provided';
|
||||
} else {
|
||||
throw e.statusCode || e.message;
|
||||
}
|
||||
});
|
||||
|
||||
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
||||
throw 'No wellknown links';
|
||||
}
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
|
||||
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
||||
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
|
||||
const link = lnik2_1 || lnik2_0 || lnik1_0;
|
||||
|
||||
if (link == null) {
|
||||
throw 'No nodeinfo link provided';
|
||||
}
|
||||
|
||||
const info = await getJson(link.href)
|
||||
.catch(e => {
|
||||
throw e.statusCode || e.message;
|
||||
});
|
||||
|
||||
logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
|
||||
|
||||
return info;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchIconUrl(instance: Instance): Promise<string | null> {
|
||||
logger.info(`Fetching icon URL of ${instance.host} ...`);
|
||||
|
||||
const url = 'https://' + instance.host;
|
||||
|
||||
const html = await getHtml(url);
|
||||
|
||||
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');
|
||||
|
||||
const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon;
|
||||
|
||||
if (href) {
|
||||
return (new URL(href, url)).href;
|
||||
}
|
||||
|
||||
const faviconUrl = url + '/favicon.ico';
|
||||
|
||||
const favicon = await fetch(faviconUrl, {
|
||||
timeout: 10000,
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
if (favicon.ok) {
|
||||
return faviconUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import { getJson } from '../misc/fetch';
|
||||
import { Instance } from '../models/entities/instance';
|
||||
import { Instances } from '../models';
|
||||
import { getNodeinfoLock } from '../misc/app-lock';
|
||||
import Logger from '../services/logger';
|
||||
|
||||
export const logger = new Logger('nodeinfo', 'cyan');
|
||||
|
||||
export async function fetchNodeinfo(instance: Instance) {
|
||||
const unlock = await getNodeinfoLock(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;
|
||||
}
|
||||
|
||||
logger.info(`Fetching nodeinfo of ${instance.host} ...`);
|
||||
|
||||
try {
|
||||
const wellknown = await getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
||||
.catch(e => {
|
||||
if (e.statusCode === 404) {
|
||||
throw 'No nodeinfo provided';
|
||||
} else {
|
||||
throw e.statusCode || e.message;
|
||||
}
|
||||
});
|
||||
|
||||
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
||||
throw 'No wellknown links';
|
||||
}
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
|
||||
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
||||
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
|
||||
const link = lnik2_1 || lnik2_0 || lnik1_0;
|
||||
|
||||
if (link == null) {
|
||||
throw 'No nodeinfo link provided';
|
||||
}
|
||||
|
||||
const info = await getJson(link.href)
|
||||
.catch(e => {
|
||||
throw e.statusCode || e.message;
|
||||
});
|
||||
|
||||
await Instances.update(instance.id, {
|
||||
infoUpdatedAt: new Date(),
|
||||
softwareName: info.software.name.toLowerCase(),
|
||||
softwareVersion: info.software.version,
|
||||
openRegistrations: info.openRegistrations,
|
||||
name: info.metadata ? (info.metadata.nodeName || info.metadata.name || null) : null,
|
||||
description: info.metadata ? (info.metadata.nodeDescription || info.metadata.description || null) : null,
|
||||
maintainerName: info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null,
|
||||
maintainerEmail: info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null,
|
||||
});
|
||||
|
||||
logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`);
|
||||
|
||||
await Instances.update(instance.id, {
|
||||
infoUpdatedAt: new Date(),
|
||||
});
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
avgColor: undefined
|
||||
blurhash: undefined
|
||||
});
|
||||
}));
|
||||
|
||||
@ -43,7 +43,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
avgColor: [ 181, 99, 106 ]
|
||||
blurhash: 'yFLxJjH[NE}@^PRiN_}Y=aVZNvFxxZ#SwIt7Eg%KIp-ospv~Nex[R6t3xZI:iwt6kWxDafoySgsAfR$*oyM|S2t7$iV[tQNbaKn%xt'
|
||||
});
|
||||
}));
|
||||
|
||||
@ -60,7 +60,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 249, 253, 250 ]
|
||||
blurhash: 'y8S?Mr-;=~~Xs;%foL?bWVs;xbR%NFay^ms;I-InI-xbs;%gofj[I-s;-WxbI-WUayxb$,NFR*~Wa{R%xbayNFI.oMj[oMNFWB$,WU'
|
||||
});
|
||||
}));
|
||||
|
||||
@ -77,7 +77,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 249, 253, 250 ]
|
||||
blurhash: 'y8S?Mr-;=~~Xs;%foL?bWVs;xbR%NFay^ms;I-InI-xbs;%gofj[I-s;-WxbI-WUayxb$,NFR*~Wa{R%xbayNFI.oMj[oMNFWB$,WU'
|
||||
});
|
||||
}));
|
||||
|
||||
@ -94,7 +94,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 255, 255, 255 ]
|
||||
blurhash: 'y74P29kDpdp{k?VDZ#krkCaefkf6fQf5HXZ$krkqadaKaJkCaKkXfkkCf5fkQ8kXZ#VDaKk?krZ~kCf6kDf6f5f6U]krZ#Z#aekrkq'
|
||||
});
|
||||
}));
|
||||
|
||||
@ -111,7 +111,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 255, 255, 255 ]
|
||||
blurhash: 'yMEKyd1U1?=nZN-2EwofR*oHnijYX6S50J=m]WEVl9JE$SR*xHR;XSX8nQxB-WS6Nts*aKskWnaxR%s*i_n~X6S5=#NgOAs*enoIWU'
|
||||
});
|
||||
}));
|
||||
|
||||
@ -129,7 +129,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
avgColor: [ 255, 255, 255 ]
|
||||
blurhash: 'yMEKyd1U1?=nZN-2EwofR*oHnijYX6S50J=m]WEVl9JE$SR*xHR;XSX8nQxB-WS6Nts*aKskWnaxR%s*i_n~X6S5=#NgOAs*enoIWU'
|
||||
});
|
||||
}));
|
||||
|
||||
@ -146,7 +146,7 @@ describe('Get file info', () => {
|
||||
},
|
||||
width: 25000,
|
||||
height: 25000,
|
||||
avgColor: undefined
|
||||
blurhash: undefined
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
27
yarn.lock
27
yarn.lock
@ -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"
|
||||
@ -1200,9 +1200,9 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
|
||||
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
|
||||
|
||||
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.5.5:
|
||||
version "6.12.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
|
||||
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
|
||||
version "6.12.3"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
|
||||
integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-json-stable-stringify "^2.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"
|
||||
|
Reference in New Issue
Block a user