mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-25 04:53:10 +02:00
Merge branch 'develop'
This commit is contained in:
commit
4c8a1867f0
58 changed files with 1261 additions and 955 deletions
|
@ -9,6 +9,14 @@
|
||||||
You should also include the user name that made the change.
|
You should also include the user name that made the change.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 12.114.0 (2022/07/15)
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- RSSティッカーで表示順序をシャッフルできるように @syuilo
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- クライアントが起動しなくなることがある問題を修正 @syuilo
|
||||||
|
|
||||||
## 12.113.0 (2022/07/13)
|
## 12.113.0 (2022/07/13)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
|
@ -808,6 +808,7 @@ reverse: "اقلب"
|
||||||
colored: "ملوّن"
|
colored: "ملوّن"
|
||||||
label: "التسمية"
|
label: "التسمية"
|
||||||
localOnly: "المحلي فقط"
|
localOnly: "المحلي فقط"
|
||||||
|
account: "الحسابات"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "هذا البريد الإلكتروني مستخدم"
|
used: "هذا البريد الإلكتروني مستخدم"
|
||||||
format: "صيغة البريد الإلكتروني غير صالحة"
|
format: "صيغة البريد الإلكتروني غير صالحة"
|
||||||
|
|
|
@ -848,6 +848,7 @@ reverse: "উল্টান"
|
||||||
colored: "রঙ্গিন"
|
colored: "রঙ্গিন"
|
||||||
label: "লেবেল"
|
label: "লেবেল"
|
||||||
localOnly: "শুধুমাত্র লোকাল"
|
localOnly: "শুধুমাত্র লোকাল"
|
||||||
|
account: "অ্যাকাউন্টগুলি"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে"
|
used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে"
|
||||||
format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি"
|
format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি"
|
||||||
|
|
|
@ -887,6 +887,9 @@ beta: "Beta"
|
||||||
enableAutoSensitive: "NSFW-Automarkierung"
|
enableAutoSensitive: "NSFW-Automarkierung"
|
||||||
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch NSFW-Markierungen für Medien, die NSFW-Anteile beinhalten. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
|
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch NSFW-Markierungen für Medien, die NSFW-Anteile beinhalten. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
|
||||||
activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft."
|
activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft."
|
||||||
|
navbar: "Navigationsleiste"
|
||||||
|
shuffle: "Mischen"
|
||||||
|
account: "Benutzerkonten"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
||||||
sensitivity: "Erkennungssensitivität"
|
sensitivity: "Erkennungssensitivität"
|
||||||
|
|
|
@ -887,6 +887,9 @@ beta: "Beta"
|
||||||
enableAutoSensitive: "Automatic NSFW-Marking"
|
enableAutoSensitive: "Automatic NSFW-Marking"
|
||||||
enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
|
enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
|
||||||
activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated."
|
activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated."
|
||||||
|
navbar: "Navigation bar"
|
||||||
|
shuffle: "Shuffle"
|
||||||
|
account: "Accounts"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||||
sensitivity: "Detection sensitivity"
|
sensitivity: "Detection sensitivity"
|
||||||
|
|
|
@ -865,6 +865,7 @@ reverse: "Echar de un capirotazo"
|
||||||
colored: "Color"
|
colored: "Color"
|
||||||
label: "Etiqueta"
|
label: "Etiqueta"
|
||||||
localOnly: "Solo local"
|
localOnly: "Solo local"
|
||||||
|
account: "Cuentas"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "Ya fue usado"
|
used: "Ya fue usado"
|
||||||
format: "Formato no válido."
|
format: "Formato no válido."
|
||||||
|
|
|
@ -843,6 +843,7 @@ reverse: "Inverser"
|
||||||
colored: "Coloré"
|
colored: "Coloré"
|
||||||
label: "Étiquette"
|
label: "Étiquette"
|
||||||
localOnly: "Local seulement"
|
localOnly: "Local seulement"
|
||||||
|
account: "Comptes"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "Non disponible"
|
used: "Non disponible"
|
||||||
format: "Le format de cette adresse de courriel est invalide"
|
format: "Le format de cette adresse de courriel est invalide"
|
||||||
|
|
|
@ -852,6 +852,7 @@ reverse: "Balik"
|
||||||
colored: "Diwarnai"
|
colored: "Diwarnai"
|
||||||
label: "Label"
|
label: "Label"
|
||||||
localOnly: "Hanya lokal"
|
localOnly: "Hanya lokal"
|
||||||
|
account: "Akun"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "Alamat surel ini telah digunakan"
|
used: "Alamat surel ini telah digunakan"
|
||||||
format: "Format tidak valid."
|
format: "Format tidak valid."
|
||||||
|
|
|
@ -814,6 +814,7 @@ reverse: "Inverti"
|
||||||
colored: "Colorato"
|
colored: "Colorato"
|
||||||
label: "Etichetta"
|
label: "Etichetta"
|
||||||
localOnly: "Soltanto locale"
|
localOnly: "Soltanto locale"
|
||||||
|
account: "Account"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "Email già in uso"
|
used: "Email già in uso"
|
||||||
format: "Formato email non valido"
|
format: "Formato email non valido"
|
||||||
|
|
|
@ -887,6 +887,9 @@ beta: "ベータ"
|
||||||
enableAutoSensitive: "自動NSFW判定"
|
enableAutoSensitive: "自動NSFW判定"
|
||||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、インスタンスによっては自動で設定されることがあります。"
|
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、インスタンスによっては自動で設定されることがあります。"
|
||||||
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
||||||
|
navbar: "ナビゲーションバー"
|
||||||
|
shuffle: "シャッフル"
|
||||||
|
account: "アカウント"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||||
|
|
|
@ -57,6 +57,7 @@ selectAccount: "Fren amiḍan"
|
||||||
accounts: "Imiḍan"
|
accounts: "Imiḍan"
|
||||||
searchByGoogle: "Nadi"
|
searchByGoogle: "Nadi"
|
||||||
file: "Ifuyla"
|
file: "Ifuyla"
|
||||||
|
account: "Imiḍan"
|
||||||
_email:
|
_email:
|
||||||
_follow:
|
_follow:
|
||||||
title: "Yeṭṭafaṛ-ik·em-id"
|
title: "Yeṭṭafaṛ-ik·em-id"
|
||||||
|
|
|
@ -886,6 +886,7 @@ beta: "베타"
|
||||||
enableAutoSensitive: "자동 NSFW 탐지"
|
enableAutoSensitive: "자동 NSFW 탐지"
|
||||||
enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다."
|
enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다."
|
||||||
activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다."
|
activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다."
|
||||||
|
account: "계정"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
||||||
sensitivity: "탐지 민감도"
|
sensitivity: "탐지 민감도"
|
||||||
|
|
|
@ -764,6 +764,7 @@ file: "Pliki"
|
||||||
reverse: "Odwróć"
|
reverse: "Odwróć"
|
||||||
colored: "Kolorowe"
|
colored: "Kolorowe"
|
||||||
label: "Etykieta"
|
label: "Etykieta"
|
||||||
|
account: "Konta"
|
||||||
_ffVisibility:
|
_ffVisibility:
|
||||||
public: "Publikuj"
|
public: "Publikuj"
|
||||||
_ad:
|
_ad:
|
||||||
|
|
|
@ -842,6 +842,7 @@ reverse: "Переворот"
|
||||||
colored: "Выделена цветом"
|
colored: "Выделена цветом"
|
||||||
label: "Метка"
|
label: "Метка"
|
||||||
localOnly: "Локально"
|
localOnly: "Локально"
|
||||||
|
account: "Учётные записи"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
||||||
setSensitiveFlagAutomatically: "Установить флаг NSFW"
|
setSensitiveFlagAutomatically: "Установить флаг NSFW"
|
||||||
|
|
|
@ -883,6 +883,8 @@ beta: "Beta"
|
||||||
enableAutoSensitive: "Automatická detekcia NSFW"
|
enableAutoSensitive: "Automatická detekcia NSFW"
|
||||||
enableAutoSensitiveDescription: "Ak je zapnuté, príznak NSFW sa na médiách automaticky nastaví pomocou strojového učenia. Aj keď je táto funkcia vypnutá, v niektorých prípadoch sa môže nastaviť automaticky."
|
enableAutoSensitiveDescription: "Ak je zapnuté, príznak NSFW sa na médiách automaticky nastaví pomocou strojového učenia. Aj keď je táto funkcia vypnutá, v niektorých prípadoch sa môže nastaviť automaticky."
|
||||||
activeEmailValidationDescription: "Dôkladnejšie overí e-mailovú adresu používateľa tým, že zistí, či ide o vyradenú e-mailovú adresu a či sa s ňou dá skutočne komunikovať. Ak nie je začiarknuté, e-mailová adresa sa kontroluje len ako text."
|
activeEmailValidationDescription: "Dôkladnejšie overí e-mailovú adresu používateľa tým, že zistí, či ide o vyradenú e-mailovú adresu a či sa s ňou dá skutočne komunikovať. Ak nie je začiarknuté, e-mailová adresa sa kontroluje len ako text."
|
||||||
|
navbar: "Navigačný panel"
|
||||||
|
account: "Účty"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
|
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
|
||||||
sensitivity: "Citlivosť detekcie"
|
sensitivity: "Citlivosť detekcie"
|
||||||
|
|
|
@ -822,6 +822,7 @@ deleteAccountConfirm: "การดำเนินการนี้จะลบ
|
||||||
incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
|
incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
|
||||||
searchByGoogle: "ค้นหา"
|
searchByGoogle: "ค้นหา"
|
||||||
file: "ไฟล์"
|
file: "ไฟล์"
|
||||||
|
account: "บัญชีผู้ใช้"
|
||||||
_ffVisibility:
|
_ffVisibility:
|
||||||
public: "เผยแพร่"
|
public: "เผยแพร่"
|
||||||
_ad:
|
_ad:
|
||||||
|
|
|
@ -887,6 +887,9 @@ beta: "Beta"
|
||||||
enableAutoSensitive: "Tự động đánh dấu NSFW"
|
enableAutoSensitive: "Tự động đánh dấu NSFW"
|
||||||
enableAutoSensitiveDescription: "Cho phép tự động phát hiện và đánh dấu media NSFW thông qua học máy, nếu có thể. Ngay cả khi tùy chọn này bị tắt, nó vẫn có thể được bật trên toàn máy chủ."
|
enableAutoSensitiveDescription: "Cho phép tự động phát hiện và đánh dấu media NSFW thông qua học máy, nếu có thể. Ngay cả khi tùy chọn này bị tắt, nó vẫn có thể được bật trên toàn máy chủ."
|
||||||
activeEmailValidationDescription: "Cho phép xác minh địa chỉ email chặt chẽ hơn, bao gồm việc kiểm tra các địa chỉ dùng một lần và xem nó có thực sự được giao tiếp hay không. Khi bỏ chọn, chỉ định dạng của email được xác minh."
|
activeEmailValidationDescription: "Cho phép xác minh địa chỉ email chặt chẽ hơn, bao gồm việc kiểm tra các địa chỉ dùng một lần và xem nó có thực sự được giao tiếp hay không. Khi bỏ chọn, chỉ định dạng của email được xác minh."
|
||||||
|
navbar: "Thanh điều hướng"
|
||||||
|
shuffle: "Xáo trộn"
|
||||||
|
account: "Tài khoản của bạn"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ."
|
description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ."
|
||||||
sensitivity: "Phát hiện nhạy cảm"
|
sensitivity: "Phát hiện nhạy cảm"
|
||||||
|
|
|
@ -878,6 +878,7 @@ cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法
|
||||||
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
|
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
|
||||||
beta: "测试"
|
beta: "测试"
|
||||||
enableAutoSensitive: "自动 NSFW 识别"
|
enableAutoSensitive: "自动 NSFW 识别"
|
||||||
|
account: "账户"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
|
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
|
||||||
sensitivity: "检测敏感度"
|
sensitivity: "检测敏感度"
|
||||||
|
|
|
@ -887,6 +887,8 @@ beta: "Beta"
|
||||||
enableAutoSensitive: "自動NSFW判定"
|
enableAutoSensitive: "自動NSFW判定"
|
||||||
enableAutoSensitiveDescription: "如果可用,請利用機器學習在媒體上自動設置 NSFW 旗標。 即使關閉此功能,依實例而定也可能會自動設置。"
|
enableAutoSensitiveDescription: "如果可用,請利用機器學習在媒體上自動設置 NSFW 旗標。 即使關閉此功能,依實例而定也可能會自動設置。"
|
||||||
activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,判斷它是否為免洗地址,或者它是否可以通信。 若關閉,則只會檢查字元是否正確。"
|
activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,判斷它是否為免洗地址,或者它是否可以通信。 若關閉,則只會檢查字元是否正確。"
|
||||||
|
navbar: "導覽列"
|
||||||
|
account: "帳戶"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
||||||
sensitivity: "檢測敏感度"
|
sensitivity: "檢測敏感度"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "12.113.0",
|
"version": "12.114.0",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -14,9 +14,11 @@
|
||||||
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
||||||
(async () => {
|
(async () => {
|
||||||
window.onerror = (e) => {
|
window.onerror = (e) => {
|
||||||
|
console.error(e);
|
||||||
renderError('SOMETHING_HAPPENED', e);
|
renderError('SOMETHING_HAPPENED', e);
|
||||||
};
|
};
|
||||||
window.onunhandledrejection = (e) => {
|
window.onunhandledrejection = (e) => {
|
||||||
|
console.error(e);
|
||||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,18 +49,30 @@
|
||||||
localStorage.setItem('localeVersion', v);
|
localStorage.setItem('localeVersion', v);
|
||||||
} else {
|
} else {
|
||||||
await checkUpdate();
|
await checkUpdate();
|
||||||
renderError('LOCALE_FETCH_FAILED');
|
renderError('LOCALE_FETCH');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Script
|
//#region Script
|
||||||
import(`/assets/${CLIENT_ENTRY}`)
|
function importAppScript() {
|
||||||
.catch(async e => {
|
import(`/assets/${CLIENT_ENTRY}`)
|
||||||
await checkUpdate();
|
.catch(async e => {
|
||||||
renderError('APP_FETCH_FAILED', e);
|
await checkUpdate();
|
||||||
})
|
console.error(e);
|
||||||
|
renderError('APP_IMPORT', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
importAppScript();
|
||||||
|
} else {
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
importAppScript();
|
||||||
|
});
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Theme
|
//#region Theme
|
||||||
|
@ -112,35 +126,35 @@
|
||||||
let errorsElement = document.getElementById('errors');
|
let errorsElement = document.getElementById('errors');
|
||||||
|
|
||||||
if (!errorsElement) {
|
if (!errorsElement) {
|
||||||
document.documentElement.innerHTML = `
|
document.body.innerHTML = `
|
||||||
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<path d="M12 9v2m0 4v.01"></path>
|
<path d="M12 9v2m0 4v.01"></path>
|
||||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h1>An error has occurred!</h1>
|
<h1>An error has occurred!</h1>
|
||||||
<button class="button-big" onclick="location.reload(true);">
|
<button class="button-big" onclick="location.reload(true);">
|
||||||
<span class="button-label-big">Refresh</span>
|
<span class="button-label-big">Refresh</span>
|
||||||
</button>
|
</button>
|
||||||
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
||||||
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
|
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
|
||||||
<a href="/flush">
|
<a href="/flush">
|
||||||
<button class="button-small">
|
<button class="button-small">
|
||||||
<span class="button-label-small">Clear preferences and cache</span>
|
<span class="button-label-small">Clear preferences and cache</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a href="/cli">
|
<a href="/cli">
|
||||||
<button class="button-small">
|
<button class="button-small">
|
||||||
<span class="button-label-small">Start the simple client</span>
|
<span class="button-label-small">Start the simple client</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a href="/bios">
|
<a href="/bios">
|
||||||
<button class="button-small">
|
<button class="button-small">
|
||||||
<span class="button-label-small">Start the repair tool</span>
|
<span class="button-label-small">Start the repair tool</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<div id="errors"></div>
|
<div id="errors"></div>
|
||||||
`;
|
`;
|
||||||
|
@ -269,17 +283,22 @@
|
||||||
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
// eslint-disable-next-line no-inner-declarations
|
||||||
async function checkUpdate() {
|
async function checkUpdate() {
|
||||||
// TODO: サーバーが落ちている場合などのエラーハンドリング
|
try {
|
||||||
const res = await fetch('/api/meta', {
|
const res = await fetch('/api/meta', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
cache: 'no-cache'
|
cache: 'no-cache'
|
||||||
});
|
});
|
||||||
|
|
||||||
const meta = await res.json();
|
const meta = await res.json();
|
||||||
|
|
||||||
if (meta.version != v) {
|
if (meta.version != v) {
|
||||||
localStorage.setItem('v', meta.version);
|
localStorage.setItem('v', meta.version);
|
||||||
refresh();
|
refresh();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
renderError('UPDATE_CHECK', e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,6 @@
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rndstr": "1.0.0",
|
"rndstr": "1.0.0",
|
||||||
"rollup": "2.76.0",
|
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sass": "1.53.0",
|
"sass": "1.53.0",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
|
@ -102,6 +101,7 @@
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.30.6",
|
"@typescript-eslint/eslint-plugin": "5.30.6",
|
||||||
"@typescript-eslint/parser": "5.30.6",
|
"@typescript-eslint/parser": "5.30.6",
|
||||||
|
"rollup": "2.76.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "10.3.0",
|
"cypress": "10.3.0",
|
||||||
"eslint": "8.19.0",
|
"eslint": "8.19.0",
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
</div>
|
</div>
|
||||||
<button class="menu _button" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
|
<button class="menu _button" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
|
||||||
</nav>
|
</nav>
|
||||||
<div ref="main" class="main"
|
<div
|
||||||
|
ref="main" class="main"
|
||||||
:class="{ uploading: uploadings.length > 0, fetching }"
|
:class="{ uploading: uploadings.length > 0, fetching }"
|
||||||
@dragover.prevent.stop="onDragover"
|
@dragover.prevent.stop="onDragover"
|
||||||
@dragenter="onDragenter"
|
@dragenter="onDragenter"
|
||||||
|
@ -142,7 +143,7 @@ const isDragSource = ref(false);
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const ilFilesObserver = new IntersectionObserver(
|
const ilFilesObserver = new IntersectionObserver(
|
||||||
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
|
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(folder, () => emit('cd', folder.value));
|
watch(folder, () => emit('cd', folder.value));
|
||||||
|
@ -232,7 +233,7 @@ function onDrop(ev: DragEvent): any {
|
||||||
removeFile(file.id);
|
removeFile(file.id);
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: folder.value ? folder.value.id : null
|
folderId: folder.value ? folder.value.id : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -248,7 +249,7 @@ function onDrop(ev: DragEvent): any {
|
||||||
removeFolder(droppedFolder.id);
|
removeFolder(droppedFolder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
folderId: droppedFolder.id,
|
folderId: droppedFolder.id,
|
||||||
parentId: folder.value ? folder.value.id : null
|
parentId: folder.value ? folder.value.id : null,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// noop
|
// noop
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
@ -256,13 +257,13 @@ function onDrop(ev: DragEvent): any {
|
||||||
case 'detected-circular-definition':
|
case 'detected-circular-definition':
|
||||||
os.alert({
|
os.alert({
|
||||||
title: i18n.ts.unableToProcess,
|
title: i18n.ts.unableToProcess,
|
||||||
text: i18n.ts.circularReferenceFolder
|
text: i18n.ts.circularReferenceFolder,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: i18n.ts.somethingHappened
|
text: i18n.ts.somethingHappened,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -278,17 +279,17 @@ function urlUpload() {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: i18n.ts.uploadFromUrl,
|
title: i18n.ts.uploadFromUrl,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
placeholder: i18n.ts.uploadFromUrlDescription
|
placeholder: i18n.ts.uploadFromUrlDescription,
|
||||||
}).then(({ canceled, result: url }) => {
|
}).then(({ canceled, result: url }) => {
|
||||||
if (canceled || !url) return;
|
if (canceled || !url) return;
|
||||||
os.api('drive/files/upload-from-url', {
|
os.api('drive/files/upload-from-url', {
|
||||||
url: url,
|
url: url,
|
||||||
folderId: folder.value ? folder.value.id : undefined
|
folderId: folder.value ? folder.value.id : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
os.alert({
|
os.alert({
|
||||||
title: i18n.ts.uploadFromUrlRequested,
|
title: i18n.ts.uploadFromUrlRequested,
|
||||||
text: i18n.ts.uploadFromUrlMayTakeTime
|
text: i18n.ts.uploadFromUrlMayTakeTime,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -296,12 +297,12 @@ function urlUpload() {
|
||||||
function createFolder() {
|
function createFolder() {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: i18n.ts.createFolder,
|
title: i18n.ts.createFolder,
|
||||||
placeholder: i18n.ts.folderName
|
placeholder: i18n.ts.folderName,
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/create', {
|
os.api('drive/folders/create', {
|
||||||
name: name,
|
name: name,
|
||||||
parentId: folder.value ? folder.value.id : undefined
|
parentId: folder.value ? folder.value.id : undefined,
|
||||||
}).then(createdFolder => {
|
}).then(createdFolder => {
|
||||||
addFolder(createdFolder, true);
|
addFolder(createdFolder, true);
|
||||||
});
|
});
|
||||||
|
@ -312,12 +313,12 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: i18n.ts.renameFolder,
|
title: i18n.ts.renameFolder,
|
||||||
placeholder: i18n.ts.inputNewFolderName,
|
placeholder: i18n.ts.inputNewFolderName,
|
||||||
default: folderToRename.name
|
default: folderToRename.name,
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
folderId: folderToRename.id,
|
folderId: folderToRename.id,
|
||||||
name: name
|
name: name,
|
||||||
}).then(updatedFolder => {
|
}).then(updatedFolder => {
|
||||||
// FIXME: 画面を更新するために自分自身に移動
|
// FIXME: 画面を更新するために自分自身に移動
|
||||||
move(updatedFolder);
|
move(updatedFolder);
|
||||||
|
@ -327,7 +328,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
|
||||||
|
|
||||||
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
||||||
os.api('drive/folders/delete', {
|
os.api('drive/folders/delete', {
|
||||||
folderId: folderToDelete.id
|
folderId: folderToDelete.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// 削除時に親フォルダに移動
|
// 削除時に親フォルダに移動
|
||||||
move(folderToDelete.parentId);
|
move(folderToDelete.parentId);
|
||||||
|
@ -337,15 +338,15 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: i18n.ts.unableToDelete,
|
title: i18n.ts.unableToDelete,
|
||||||
text: i18n.ts.hasChildFilesOrFolders
|
text: i18n.ts.hasChildFilesOrFolders,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: i18n.ts.unableToDelete
|
text: i18n.ts.unableToDelete,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,7 +412,7 @@ function move(target?: Misskey.entities.DriveFolder) {
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
os.api('drive/folders/show', {
|
os.api('drive/folders/show', {
|
||||||
folderId: target
|
folderId: target,
|
||||||
}).then(folderToMove => {
|
}).then(folderToMove => {
|
||||||
folder.value = folderToMove;
|
folder.value = folderToMove;
|
||||||
hierarchyFolders.value = [];
|
hierarchyFolders.value = [];
|
||||||
|
@ -510,7 +511,7 @@ async function fetch() {
|
||||||
|
|
||||||
const foldersPromise = os.api('drive/folders', {
|
const foldersPromise = os.api('drive/folders', {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
limit: foldersMax + 1
|
limit: foldersMax + 1,
|
||||||
}).then(fetchedFolders => {
|
}).then(fetchedFolders => {
|
||||||
if (fetchedFolders.length === foldersMax + 1) {
|
if (fetchedFolders.length === foldersMax + 1) {
|
||||||
moreFolders.value = true;
|
moreFolders.value = true;
|
||||||
|
@ -522,7 +523,7 @@ async function fetch() {
|
||||||
const filesPromise = os.api('drive/files', {
|
const filesPromise = os.api('drive/files', {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
limit: filesMax + 1
|
limit: filesMax + 1,
|
||||||
}).then(fetchedFiles => {
|
}).then(fetchedFiles => {
|
||||||
if (fetchedFiles.length === filesMax + 1) {
|
if (fetchedFiles.length === filesMax + 1) {
|
||||||
moreFiles.value = true;
|
moreFiles.value = true;
|
||||||
|
@ -549,7 +550,7 @@ function fetchMoreFiles() {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
untilId: files.value[files.value.length - 1].id,
|
untilId: files.value[files.value.length - 1].id,
|
||||||
limit: max + 1
|
limit: max + 1,
|
||||||
}).then(files => {
|
}).then(files => {
|
||||||
if (files.length === max + 1) {
|
if (files.length === max + 1) {
|
||||||
moreFiles.value = true;
|
moreFiles.value = true;
|
||||||
|
@ -569,30 +570,30 @@ function getMenu() {
|
||||||
ref: keepOriginal,
|
ref: keepOriginal,
|
||||||
}, null, {
|
}, null, {
|
||||||
text: i18n.ts.addFile,
|
text: i18n.ts.addFile,
|
||||||
type: 'label'
|
type: 'label',
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts.upload,
|
text: i18n.ts.upload,
|
||||||
icon: 'fas fa-upload',
|
icon: 'fas fa-upload',
|
||||||
action: () => { selectLocalFile(); }
|
action: () => { selectLocalFile(); },
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts.fromUrl,
|
text: i18n.ts.fromUrl,
|
||||||
icon: 'fas fa-link',
|
icon: 'fas fa-link',
|
||||||
action: () => { urlUpload(); }
|
action: () => { urlUpload(); },
|
||||||
}, null, {
|
}, null, {
|
||||||
text: folder.value ? folder.value.name : i18n.ts.drive,
|
text: folder.value ? folder.value.name : i18n.ts.drive,
|
||||||
type: 'label'
|
type: 'label',
|
||||||
}, folder.value ? {
|
}, folder.value ? {
|
||||||
text: i18n.ts.renameFolder,
|
text: i18n.ts.renameFolder,
|
||||||
icon: 'fas fa-i-cursor',
|
icon: 'fas fa-i-cursor',
|
||||||
action: () => { renameFolder(folder.value); }
|
action: () => { renameFolder(folder.value); },
|
||||||
} : undefined, folder.value ? {
|
} : undefined, folder.value ? {
|
||||||
text: i18n.ts.deleteFolder,
|
text: i18n.ts.deleteFolder,
|
||||||
icon: 'fas fa-trash-alt',
|
icon: 'fas fa-trash-alt',
|
||||||
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
|
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
|
||||||
} : undefined, {
|
} : undefined, {
|
||||||
text: i18n.ts.createFolder,
|
text: i18n.ts.createFolder,
|
||||||
icon: 'fas fa-folder-plus',
|
icon: 'fas fa-folder-plus',
|
||||||
action: () => { createFolder(); }
|
action: () => { createFolder(); },
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -662,14 +663,14 @@ onBeforeUnmount(() => {
|
||||||
> .path {
|
> .path {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
line-height: 50px;
|
line-height: 42px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
line-height: 50px;
|
line-height: 42px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
|
@ -16,9 +16,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useCssModule } from 'vue';
|
import { } from 'vue';
|
||||||
|
|
||||||
useCssModule();
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkModal from '@/components/ui/modal.vue';
|
import MkModal from '@/components/ui/modal.vue';
|
||||||
import { menuDef } from '@/menu';
|
import { navbarItemDef } from '@/navbar';
|
||||||
import { instanceName } from '@/config';
|
import { instanceName } from '@/config';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -62,7 +62,7 @@ const modal = $ref<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const menu = defaultStore.state.menu;
|
const menu = defaultStore.state.menu;
|
||||||
|
|
||||||
const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
||||||
type: def.to ? 'link' : 'button',
|
type: def.to ? 'link' : 'button',
|
||||||
text: i18n.ts[def.title],
|
text: i18n.ts[def.title],
|
||||||
icon: def.icon,
|
icon: def.icon,
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import { useCssModule } from 'vue';
|
import { } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { host as localHost } from '@/config';
|
import { host as localHost } from '@/config';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
@ -37,8 +37,6 @@ const isMe = $i && (
|
||||||
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
|
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
|
||||||
bg.setAlpha(0.1);
|
bg.setAlpha(0.1);
|
||||||
const bgCss = bg.toRgbString();
|
const bgCss = bg.toRgbString();
|
||||||
|
|
||||||
useCssModule();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -592,8 +592,6 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.max-width_300px {
|
&.max-width_300px {
|
||||||
font-size: 0.825em;
|
|
||||||
|
|
||||||
> .article {
|
> .article {
|
||||||
> .avatar {
|
> .avatar {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
|
|
|
@ -177,13 +177,7 @@ useTooltip(reactionRef, (showing) => {
|
||||||
|
|
||||||
&.max-width_500px {
|
&.max-width_500px {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-size: 0.8em;
|
font-size: 0.85em;
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
clear: both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .head {
|
> .head {
|
||||||
|
|
|
@ -1,255 +1,234 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||||
<template v-if="meta">
|
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
|
||||||
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
|
<template #label>{{ $ts.invitationCode }}</template>
|
||||||
<template #label>{{ $ts.invitationCode }}</template>
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
<template #prefix><i class="fas fa-key"></i></template>
|
</MkInput>
|
||||||
</MkInput>
|
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
||||||
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
<template #prefix>@</template>
|
||||||
<template #prefix>@</template>
|
<template #suffix>@{{ host }}</template>
|
||||||
<template #suffix>@{{ host }}</template>
|
<template #caption>
|
||||||
<template #caption>
|
<span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
|
||||||
<span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
|
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
|
||||||
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
|
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
|
||||||
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
|
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
|
||||||
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
|
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
|
||||||
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
|
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
|
||||||
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
|
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
|
||||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
|
</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||||
|
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
||||||
|
<template #prefix><i class="fas fa-envelope"></i></template>
|
||||||
|
<template #caption>
|
||||||
|
<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
|
||||||
|
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
|
||||||
|
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
|
||||||
|
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
|
||||||
|
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
|
||||||
|
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
|
||||||
|
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
|
||||||
|
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
|
||||||
|
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
|
||||||
|
</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||||
|
<template #label>{{ $ts.password }}</template>
|
||||||
|
<template #prefix><i class="fas fa-lock"></i></template>
|
||||||
|
<template #caption>
|
||||||
|
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
|
||||||
|
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
|
||||||
|
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
|
||||||
|
</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||||
|
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
|
||||||
|
<template #prefix><i class="fas fa-lock"></i></template>
|
||||||
|
<template #caption>
|
||||||
|
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
|
||||||
|
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
|
||||||
|
</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
|
||||||
|
<I18n :src="$ts.agreeTo">
|
||||||
|
<template #0>
|
||||||
|
<a :href="instance.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</I18n>
|
||||||
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
</MkSwitch>
|
||||||
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||||
<template #prefix><i class="fas fa-envelope"></i></template>
|
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||||
<template #caption>
|
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
|
||||||
<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
|
|
||||||
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
|
|
||||||
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
|
|
||||||
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
|
|
||||||
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
|
|
||||||
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
|
|
||||||
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
|
|
||||||
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
|
|
||||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
|
|
||||||
</template>
|
|
||||||
</MkInput>
|
|
||||||
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
|
||||||
<template #label>{{ $ts.password }}</template>
|
|
||||||
<template #prefix><i class="fas fa-lock"></i></template>
|
|
||||||
<template #caption>
|
|
||||||
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
|
|
||||||
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
|
|
||||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
|
|
||||||
</template>
|
|
||||||
</MkInput>
|
|
||||||
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
|
||||||
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
|
|
||||||
<template #prefix><i class="fas fa-lock"></i></template>
|
|
||||||
<template #caption>
|
|
||||||
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
|
|
||||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
|
|
||||||
</template>
|
|
||||||
</MkInput>
|
|
||||||
<MkSwitch v-if="meta.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
|
|
||||||
<I18n :src="$ts.agreeTo">
|
|
||||||
<template #0>
|
|
||||||
<a :href="meta.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</MkSwitch>
|
|
||||||
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
|
|
||||||
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
|
|
||||||
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
|
|
||||||
</template>
|
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { } from 'vue';
|
||||||
import getPasswordStrength from 'syuilo-password-strength';
|
import getPasswordStrength from 'syuilo-password-strength';
|
||||||
import { toUnicode } from 'punycode/';
|
import { toUnicode } from 'punycode/';
|
||||||
import MkButton from './ui/button.vue';
|
import MkButton from './ui/button.vue';
|
||||||
|
import MkCaptcha from './captcha.vue';
|
||||||
import MkInput from './form/input.vue';
|
import MkInput from './form/input.vue';
|
||||||
import MkSwitch from './form/switch.vue';
|
import MkSwitch from './form/switch.vue';
|
||||||
import { host, url } from '@/config';
|
import * as config from '@/config';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { login } from '@/account';
|
import { login } from '@/account';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
components: {
|
autoSet?: boolean;
|
||||||
MkButton,
|
}>(), {
|
||||||
MkInput,
|
autoSet: false,
|
||||||
MkSwitch,
|
|
||||||
MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')),
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
autoSet: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['signup'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
host: toUnicode(host),
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
retypedPassword: '',
|
|
||||||
invitationCode: '',
|
|
||||||
email: '',
|
|
||||||
url,
|
|
||||||
usernameState: null,
|
|
||||||
emailState: null,
|
|
||||||
passwordStrength: '',
|
|
||||||
passwordRetypeState: null,
|
|
||||||
submitting: false,
|
|
||||||
ToSAgreement: false,
|
|
||||||
hCaptchaResponse: null,
|
|
||||||
reCaptchaResponse: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
meta() {
|
|
||||||
return this.$instance;
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldDisableSubmitting(): boolean {
|
|
||||||
return this.submitting ||
|
|
||||||
this.meta.tosUrl && !this.ToSAgreement ||
|
|
||||||
this.meta.enableHcaptcha && !this.hCaptchaResponse ||
|
|
||||||
this.meta.enableRecaptcha && !this.reCaptchaResponse ||
|
|
||||||
this.passwordRetypeState === 'not-match';
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldShowProfileUrl(): boolean {
|
|
||||||
return (this.username !== '' &&
|
|
||||||
this.usernameState !== 'invalid-format' &&
|
|
||||||
this.usernameState !== 'min-range' &&
|
|
||||||
this.usernameState !== 'max-range');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onChangeUsername() {
|
|
||||||
if (this.username === '') {
|
|
||||||
this.usernameState = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const err =
|
|
||||||
!this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
|
|
||||||
this.username.length < 1 ? 'min-range' :
|
|
||||||
this.username.length > 20 ? 'max-range' :
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
this.usernameState = err;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.usernameState = 'wait';
|
|
||||||
|
|
||||||
os.api('username/available', {
|
|
||||||
username: this.username,
|
|
||||||
}).then(result => {
|
|
||||||
this.usernameState = result.available ? 'ok' : 'unavailable';
|
|
||||||
}).catch(err => {
|
|
||||||
this.usernameState = 'error';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeEmail() {
|
|
||||||
if (this.email === '') {
|
|
||||||
this.emailState = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emailState = 'wait';
|
|
||||||
|
|
||||||
os.api('email-address/available', {
|
|
||||||
emailAddress: this.email,
|
|
||||||
}).then(result => {
|
|
||||||
this.emailState = result.available ? 'ok' :
|
|
||||||
result.reason === 'used' ? 'unavailable:used' :
|
|
||||||
result.reason === 'format' ? 'unavailable:format' :
|
|
||||||
result.reason === 'disposable' ? 'unavailable:disposable' :
|
|
||||||
result.reason === 'mx' ? 'unavailable:mx' :
|
|
||||||
result.reason === 'smtp' ? 'unavailable:smtp' :
|
|
||||||
'unavailable';
|
|
||||||
}).catch(err => {
|
|
||||||
this.emailState = 'error';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangePassword() {
|
|
||||||
if (this.password === '') {
|
|
||||||
this.passwordStrength = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strength = getPasswordStrength(this.password);
|
|
||||||
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangePasswordRetype() {
|
|
||||||
if (this.retypedPassword === '') {
|
|
||||||
this.passwordRetypeState = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.passwordRetypeState = this.password === this.retypedPassword ? 'match' : 'not-match';
|
|
||||||
},
|
|
||||||
|
|
||||||
onSubmit() {
|
|
||||||
if (this.submitting) return;
|
|
||||||
this.submitting = true;
|
|
||||||
|
|
||||||
os.api('signup', {
|
|
||||||
username: this.username,
|
|
||||||
password: this.password,
|
|
||||||
emailAddress: this.email,
|
|
||||||
invitationCode: this.invitationCode,
|
|
||||||
'hcaptcha-response': this.hCaptchaResponse,
|
|
||||||
'g-recaptcha-response': this.reCaptchaResponse,
|
|
||||||
}).then(() => {
|
|
||||||
if (this.meta.emailRequiredForSignup) {
|
|
||||||
os.alert({
|
|
||||||
type: 'success',
|
|
||||||
title: this.$ts._signup.almostThere,
|
|
||||||
text: this.$t('_signup.emailSent', { email: this.email }),
|
|
||||||
});
|
|
||||||
this.$emit('signupEmailPending');
|
|
||||||
} else {
|
|
||||||
os.api('signin', {
|
|
||||||
username: this.username,
|
|
||||||
password: this.password,
|
|
||||||
}).then(res => {
|
|
||||||
this.$emit('signup', res);
|
|
||||||
|
|
||||||
if (this.autoSet) {
|
|
||||||
login(res.i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
this.submitting = false;
|
|
||||||
this.$refs.hcaptcha?.reset?.();
|
|
||||||
this.$refs.recaptcha?.reset?.();
|
|
||||||
|
|
||||||
os.alert({
|
|
||||||
type: 'error',
|
|
||||||
text: this.$ts.somethingHappened,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'signup', user: Record<string, any>): void;
|
||||||
|
(ev: 'signupEmailPending'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const host = toUnicode(config.host);
|
||||||
|
|
||||||
|
let hcaptcha = $ref();
|
||||||
|
let recaptcha = $ref();
|
||||||
|
|
||||||
|
let username: string = $ref('');
|
||||||
|
let password: string = $ref('');
|
||||||
|
let retypedPassword: string = $ref('');
|
||||||
|
let invitationCode: string = $ref('');
|
||||||
|
let email = $ref('');
|
||||||
|
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
|
||||||
|
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
|
||||||
|
let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
|
||||||
|
let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
|
||||||
|
let submitting: boolean = $ref(false);
|
||||||
|
let ToSAgreement: boolean = $ref(false);
|
||||||
|
let hCaptchaResponse = $ref(null);
|
||||||
|
let reCaptchaResponse = $ref(null);
|
||||||
|
|
||||||
|
const shouldDisableSubmitting = $computed((): boolean => {
|
||||||
|
return submitting ||
|
||||||
|
instance.tosUrl && !ToSAgreement ||
|
||||||
|
instance.enableHcaptcha && !hCaptchaResponse ||
|
||||||
|
instance.enableRecaptcha && !reCaptchaResponse ||
|
||||||
|
passwordRetypeState === 'not-match';
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChangeUsername(): void {
|
||||||
|
if (username === '') {
|
||||||
|
usernameState = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const err =
|
||||||
|
!username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
|
||||||
|
username.length < 1 ? 'min-range' :
|
||||||
|
username.length > 20 ? 'max-range' :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
usernameState = err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameState = 'wait';
|
||||||
|
|
||||||
|
os.api('username/available', {
|
||||||
|
username,
|
||||||
|
}).then(result => {
|
||||||
|
usernameState = result.available ? 'ok' : 'unavailable';
|
||||||
|
}).catch(() => {
|
||||||
|
usernameState = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeEmail(): void {
|
||||||
|
if (email === '') {
|
||||||
|
emailState = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emailState = 'wait';
|
||||||
|
|
||||||
|
os.api('email-address/available', {
|
||||||
|
emailAddress: email,
|
||||||
|
}).then(result => {
|
||||||
|
emailState = result.available ? 'ok' :
|
||||||
|
result.reason === 'used' ? 'unavailable:used' :
|
||||||
|
result.reason === 'format' ? 'unavailable:format' :
|
||||||
|
result.reason === 'disposable' ? 'unavailable:disposable' :
|
||||||
|
result.reason === 'mx' ? 'unavailable:mx' :
|
||||||
|
result.reason === 'smtp' ? 'unavailable:smtp' :
|
||||||
|
'unavailable';
|
||||||
|
}).catch(() => {
|
||||||
|
emailState = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangePassword(): void {
|
||||||
|
if (password === '') {
|
||||||
|
passwordStrength = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strength = getPasswordStrength(password);
|
||||||
|
passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangePasswordRetype(): void {
|
||||||
|
if (retypedPassword === '') {
|
||||||
|
passwordRetypeState = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordRetypeState = password === retypedPassword ? 'match' : 'not-match';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(): void {
|
||||||
|
if (submitting) return;
|
||||||
|
submitting = true;
|
||||||
|
|
||||||
|
os.api('signup', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
emailAddress: email,
|
||||||
|
invitationCode,
|
||||||
|
'hcaptcha-response': hCaptchaResponse,
|
||||||
|
'g-recaptcha-response': reCaptchaResponse,
|
||||||
|
}).then(() => {
|
||||||
|
if (instance.emailRequiredForSignup) {
|
||||||
|
os.alert({
|
||||||
|
type: 'success',
|
||||||
|
title: i18n.ts._signup.almostThere,
|
||||||
|
text: i18n.t('_signup.emailSent', { email }),
|
||||||
|
});
|
||||||
|
emit('signupEmailPending');
|
||||||
|
} else {
|
||||||
|
os.api('signin', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}).then(res => {
|
||||||
|
emit('signup', res);
|
||||||
|
|
||||||
|
if (props.autoSet) {
|
||||||
|
login(res.i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
submitting = false;
|
||||||
|
hcaptcha.reset?.();
|
||||||
|
recaptcha.reset?.();
|
||||||
|
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.ts.somethingHappened,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -140,7 +140,7 @@ function focusDown() {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 0.85em;
|
font-size: 0.9em;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -98,7 +98,7 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
$height: 58px;
|
$height: 46px;
|
||||||
$height-narrow: 42px;
|
$height-narrow: 42px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -138,6 +138,7 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ const setPosition = () => {
|
||||||
let top: number;
|
let top: number;
|
||||||
|
|
||||||
if (props.targetElement) {
|
if (props.targetElement) {
|
||||||
left = (rect.left + window.pageXOffset) + props.innerMargin;
|
left = (rect.left + props.targetElement.offsetWidth + window.pageXOffset) + props.innerMargin;
|
||||||
top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
|
top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
|
||||||
} else {
|
} else {
|
||||||
left = props.x + props.innerMargin;
|
left = props.x + props.innerMargin;
|
||||||
|
|
|
@ -393,7 +393,7 @@ export default defineComponent({
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
--height: 45px;
|
--height: 42px;
|
||||||
|
|
||||||
&.mini {
|
&.mini {
|
||||||
--height: 38px;
|
--height: 38px;
|
||||||
|
|
|
@ -8,7 +8,6 @@ import tooltip from './tooltip';
|
||||||
import hotkey from './hotkey';
|
import hotkey from './hotkey';
|
||||||
import appear from './appear';
|
import appear from './appear';
|
||||||
import anim from './anim';
|
import anim from './anim';
|
||||||
import stickyContainer from './sticky-container';
|
|
||||||
import clickAnime from './click-anime';
|
import clickAnime from './click-anime';
|
||||||
import panel from './panel';
|
import panel from './panel';
|
||||||
import adaptiveBorder from './adaptive-border';
|
import adaptiveBorder from './adaptive-border';
|
||||||
|
@ -24,7 +23,6 @@ export default function(app: App) {
|
||||||
app.directive('appear', appear);
|
app.directive('appear', appear);
|
||||||
app.directive('anim', anim);
|
app.directive('anim', anim);
|
||||||
app.directive('click-anime', clickAnime);
|
app.directive('click-anime', clickAnime);
|
||||||
app.directive('sticky-container', stickyContainer);
|
|
||||||
app.directive('panel', panel);
|
app.directive('panel', panel);
|
||||||
app.directive('adaptive-border', adaptiveBorder);
|
app.directive('adaptive-border', adaptiveBorder);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { Directive } from 'vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mounted(src, binding, vn) {
|
|
||||||
//const query = binding.value;
|
|
||||||
|
|
||||||
const header = src.children[0];
|
|
||||||
const body = src.children[1];
|
|
||||||
const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
|
|
||||||
src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
|
|
||||||
if (body) body.dataset.stickyContainerHeaderHeight = header.offsetHeight.toString();
|
|
||||||
header.style.setProperty('--stickyTop', currentStickyTop);
|
|
||||||
header.style.position = 'sticky';
|
|
||||||
header.style.top = 'var(--stickyTop)';
|
|
||||||
header.style.zIndex = '1';
|
|
||||||
},
|
|
||||||
} as Directive;
|
|
|
@ -6,7 +6,7 @@ import { i18n } from '@/i18n';
|
||||||
import { ui } from '@/config';
|
import { ui } from '@/config';
|
||||||
import { unisonReload } from '@/scripts/unison-reload';
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
|
|
||||||
export const menuDef = reactive({
|
export const navbarItemDef = reactive({
|
||||||
notifications: {
|
notifications: {
|
||||||
title: 'notifications',
|
title: 'notifications',
|
||||||
icon: 'fas fa-bell',
|
icon: 'fas fa-bell',
|
|
@ -56,10 +56,10 @@
|
||||||
|
|
||||||
<FormRadios v-model="fontSize" class="_formBlock">
|
<FormRadios v-model="fontSize" class="_formBlock">
|
||||||
<template #label>{{ i18n.ts.fontSize }}</template>
|
<template #label>{{ i18n.ts.fontSize }}</template>
|
||||||
<option value="small"><span style="font-size: 14px;">Aa</span></option>
|
<option :value="null"><span style="font-size: 14px;">Aa</span></option>
|
||||||
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
|
<option value="1"><span style="font-size: 15px;">Aa</span></option>
|
||||||
<option value="large"><span style="font-size: 18px;">Aa</span></option>
|
<option value="2"><span style="font-size: 16px;">Aa</span></option>
|
||||||
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
|
<option value="3"><span style="font-size: 17px;">Aa</span></option>
|
||||||
</FormRadios>
|
</FormRadios>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
|
|
@ -114,15 +114,15 @@ const menuDef = computed(() => [{
|
||||||
to: '/settings/theme',
|
to: '/settings/theme',
|
||||||
active: props.initialPage === 'theme',
|
active: props.initialPage === 'theme',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-list-ul',
|
icon: 'fas fa-bars',
|
||||||
|
text: i18n.ts.navbar,
|
||||||
|
to: '/settings/navbar',
|
||||||
|
active: props.initialPage === 'navbar',
|
||||||
|
}, {
|
||||||
|
icon: 'fas fa-bars-progress',
|
||||||
text: i18n.ts.statusbar,
|
text: i18n.ts.statusbar,
|
||||||
to: '/settings/statusbars',
|
to: '/settings/statusbars',
|
||||||
active: props.initialPage === 'statusbars',
|
active: props.initialPage === 'statusbars',
|
||||||
}, {
|
|
||||||
icon: 'fas fa-list-ul',
|
|
||||||
text: i18n.ts.menu,
|
|
||||||
to: '/settings/menu',
|
|
||||||
active: props.initialPage === 'menu',
|
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-music',
|
icon: 'fas fa-music',
|
||||||
text: i18n.ts.sounds,
|
text: i18n.ts.sounds,
|
||||||
|
@ -225,7 +225,7 @@ const component = computed(() => {
|
||||||
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
|
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
|
||||||
case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
|
case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
|
||||||
case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
|
case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
|
||||||
case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
|
case 'navbar': return defineAsyncComponent(() => import('./navbar.vue'));
|
||||||
case 'statusbars': return defineAsyncComponent(() => import('./statusbars.vue'));
|
case 'statusbars': return defineAsyncComponent(() => import('./statusbars.vue'));
|
||||||
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
|
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
|
||||||
case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
|
case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
|
||||||
|
@ -291,6 +291,8 @@ const headerActions = $computed(() => []);
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
definePageMetadata(INFO);
|
definePageMetadata(INFO);
|
||||||
|
// w 890
|
||||||
|
// h 700
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="_formRoot">
|
<div class="_formRoot">
|
||||||
<FormTextarea v-model="items" tall manual-save class="_formBlock">
|
<FormTextarea v-model="items" tall manual-save class="_formBlock">
|
||||||
<template #label>{{ i18n.ts.menu }}</template>
|
<template #label>{{ i18n.ts.navbar }}</template>
|
||||||
<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
|
<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import FormTextarea from '@/components/form/textarea.vue';
|
||||||
import FormRadios from '@/components/form/radios.vue';
|
import FormRadios from '@/components/form/radios.vue';
|
||||||
import FormButton from '@/components/ui/button.vue';
|
import FormButton from '@/components/ui/button.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { menuDef } from '@/menu';
|
import { navbarItemDef } from '@/navbar';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { unisonReload } from '@/scripts/unison-reload';
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -45,11 +45,11 @@ async function reloadAsk() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addItem() {
|
async function addItem() {
|
||||||
const menu = Object.keys(menuDef).filter(k => !defaultStore.state.menu.includes(k));
|
const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
|
||||||
const { canceled, result: item } = await os.select({
|
const { canceled, result: item } = await os.select({
|
||||||
title: i18n.ts.addItem,
|
title: i18n.ts.addItem,
|
||||||
items: [...menu.map(k => ({
|
items: [...menu.map(k => ({
|
||||||
value: k, text: i18n.ts[menuDef[k].title],
|
value: k, text: i18n.ts[navbarItemDef[k].title],
|
||||||
})), {
|
})), {
|
||||||
value: '-', text: i18n.ts.divider,
|
value: '-', text: i18n.ts.divider,
|
||||||
}],
|
}],
|
||||||
|
@ -81,7 +81,7 @@ const headerActions = $computed(() => []);
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.menu,
|
title: i18n.ts.navbar,
|
||||||
icon: 'fas fa-list-ul',
|
icon: 'fas fa-list-ul',
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts.signinHistory }}</template>
|
<template #label>{{ i18n.ts.signinHistory }}</template>
|
||||||
<MkPagination :pagination="pagination">
|
<MkPagination :pagination="pagination" disable-auto-load>
|
||||||
<template #default="{items}">
|
<template #default="{items}">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
|
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
<MkInput v-model="statusbar.props.url" manual-save class="_formBlock" type="url">
|
<MkInput v-model="statusbar.props.url" manual-save class="_formBlock" type="url">
|
||||||
<template #label>URL</template>
|
<template #label>URL</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<MkSwitch v-model="statusbar.props.shuffle" class="_formBlock">
|
||||||
|
<template #label>{{ i18n.ts.shuffle }}</template>
|
||||||
|
</MkSwitch>
|
||||||
<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
|
<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
|
||||||
<template #label>{{ i18n.ts.refreshInterval }}</template>
|
<template #label>{{ i18n.ts.refreshInterval }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@ -86,7 +89,6 @@ import FormRadios from '@/components/form/radios.vue';
|
||||||
import FormButton from '@/components/ui/button.vue';
|
import FormButton from '@/components/ui/button.vue';
|
||||||
import FormRange from '@/components/form/range.vue';
|
import FormRange from '@/components/form/range.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { menuDef } from '@/menu';
|
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
@ -101,6 +103,7 @@ watch(() => statusbar.type, () => {
|
||||||
if (statusbar.type === 'rss') {
|
if (statusbar.type === 'rss') {
|
||||||
statusbar.name = 'NEWS';
|
statusbar.name = 'NEWS';
|
||||||
statusbar.props.url = 'http://feeds.afpbb.com/rss/afpbb/afpbbnews';
|
statusbar.props.url = 'http://feeds.afpbb.com/rss/afpbb/afpbbnews';
|
||||||
|
statusbar.props.shuffle = true;
|
||||||
statusbar.props.refreshIntervalSec = 120;
|
statusbar.props.refreshIntervalSec = 120;
|
||||||
statusbar.props.display = 'marquee';
|
statusbar.props.display = 'marquee';
|
||||||
statusbar.props.marqueeDuration = 100;
|
statusbar.props.marqueeDuration = 100;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-sticky-container class="yrzkoczt">
|
<MkStickyContainer>
|
||||||
<MkTab v-model="include" class="tab">
|
<template #header>
|
||||||
<option :value="null">{{ $ts.notes }}</option>
|
<MkTab v-model="include" :class="$style.tab">
|
||||||
<option value="replies">{{ $ts.notesAndReplies }}</option>
|
<option :value="null">{{ $ts.notes }}</option>
|
||||||
<option value="files">{{ $ts.withFiles }}</option>
|
<option value="replies">{{ $ts.notesAndReplies }}</option>
|
||||||
</MkTab>
|
<option value="files">{{ $ts.withFiles }}</option>
|
||||||
|
</MkTab>
|
||||||
|
</template>
|
||||||
<XNotes :no-gap="true" :pagination="pagination"/>
|
<XNotes :no-gap="true" :pagination="pagination"/>
|
||||||
</div>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -33,12 +35,10 @@ const pagination = {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.yrzkoczt {
|
.tab {
|
||||||
> .tab {
|
margin: calc(var(--margin) / 2) 0;
|
||||||
margin: calc(var(--margin) / 2) 0;
|
padding: calc(var(--margin) / 2) 0;
|
||||||
padding: calc(var(--margin) / 2) 0;
|
background: var(--bg);
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
19
packages/client/src/scripts/shuffle.ts
Normal file
19
packages/client/src/scripts/shuffle.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* 配列をシャッフル (破壊的)
|
||||||
|
*/
|
||||||
|
export function shuffle<T extends any[]>(array: T): T {
|
||||||
|
let currentIndex = array.length, randomIndex;
|
||||||
|
|
||||||
|
// While there remain elements to shuffle.
|
||||||
|
while (currentIndex !== 0) {
|
||||||
|
// Pick a remaining element.
|
||||||
|
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||||
|
currentIndex--;
|
||||||
|
|
||||||
|
// And swap it with the current element.
|
||||||
|
[array[currentIndex], array[randomIndex]] = [
|
||||||
|
array[randomIndex], array[currentIndex]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ html {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
|
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
text-size-adjust: 100%;
|
text-size-adjust: 100%;
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
|
@ -61,16 +61,16 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.f-small {
|
&.f-1 {
|
||||||
font-size: 0.9em;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.f-large {
|
&.f-2 {
|
||||||
font-size: 1.1em;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.f-veryLarge {
|
&.f-3 {
|
||||||
font-size: 1.2em;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.useSystemFont {
|
&.useSystemFont {
|
||||||
|
|
284
packages/client/src/ui/_common_/navbar-for-mobile.vue
Normal file
284
packages/client/src/ui/_common_/navbar-for-mobile.vue
Normal file
|
@ -0,0 +1,284 @@
|
||||||
|
<template>
|
||||||
|
<div class="kmwsukvl">
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
|
||||||
|
<button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
|
||||||
|
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="middle">
|
||||||
|
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
|
||||||
|
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
||||||
|
</MkA>
|
||||||
|
<template v-for="item in menu">
|
||||||
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
|
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: navbarItemDef[item].active }]" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||||
|
<i class="icon fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
|
||||||
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
|
||||||
|
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
|
||||||
|
</MkA>
|
||||||
|
<button v-click-anime class="item _button" @click="more">
|
||||||
|
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
||||||
|
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</button>
|
||||||
|
<MkA v-click-anime class="item" active-class="active" to="/settings">
|
||||||
|
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<button class="item _button post" data-cy-open-post-form @click="os.post">
|
||||||
|
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
||||||
|
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, defineComponent, ref, toRef, watch } from 'vue';
|
||||||
|
import { host } from '@/config';
|
||||||
|
import { search } from '@/scripts/search';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const menu = toRef(defaultStore.state, 'menu');
|
||||||
|
const otherMenuItemIndicated = computed(() => {
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (menu.value.includes(def)) continue;
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAccountMenu(ev: MouseEvent) {
|
||||||
|
openAccountMenu_({
|
||||||
|
withExtraOperation: true,
|
||||||
|
}, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInstanceMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: instance.name ?? host,
|
||||||
|
type: 'label',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.instanceInfo,
|
||||||
|
icon: 'fas fa-info-circle',
|
||||||
|
to: '/about',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.customEmojis,
|
||||||
|
icon: 'fas fa-laugh',
|
||||||
|
to: '/about#emojis',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.federation,
|
||||||
|
icon: 'fas fa-globe',
|
||||||
|
to: '/about#federation',
|
||||||
|
}], ev.currentTarget ?? ev.target, {
|
||||||
|
align: 'left',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function more() {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {}, {
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.kmwsukvl {
|
||||||
|
> .body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 38px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 30px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 16px 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding-left: 24px;
|
||||||
|
line-height: 2.85rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--navFg);
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--navHoverFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--navActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
492
packages/client/src/ui/_common_/navbar.vue
Normal file
492
packages/client/src/ui/_common_/navbar.vue
Normal file
|
@ -0,0 +1,492 @@
|
||||||
|
<template>
|
||||||
|
<div class="mvcprjjd" :class="{ iconOnly }">
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
|
||||||
|
<button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
|
||||||
|
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="middle">
|
||||||
|
<MkA v-click-anime v-tooltip.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact>
|
||||||
|
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
|
||||||
|
</MkA>
|
||||||
|
<template v-for="item in menu">
|
||||||
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
|
<component
|
||||||
|
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
|
||||||
|
v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
|
||||||
|
v-click-anime
|
||||||
|
v-tooltip.right="i18n.ts[navbarItemDef[item].title]"
|
||||||
|
class="item _button"
|
||||||
|
:class="[item, { active: navbarItemDef[item].active }]"
|
||||||
|
active-class="active"
|
||||||
|
:to="navbarItemDef[item].to"
|
||||||
|
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
|
||||||
|
>
|
||||||
|
<i class="icon fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ i18n.ts[navbarItemDef[item].title] }}</span>
|
||||||
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin">
|
||||||
|
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||||
|
</MkA>
|
||||||
|
<button v-click-anime class="item _button" @click="more">
|
||||||
|
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ i18n.ts.more }}</span>
|
||||||
|
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</button>
|
||||||
|
<MkA v-click-anime v-tooltip.right="i18n.ts.settings" class="item" active-class="active" to="/settings">
|
||||||
|
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<button v-tooltip.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post">
|
||||||
|
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime v-tooltip.right="i18n.ts.account" class="item _button account" @click="openAccountMenu">
|
||||||
|
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { host } from '@/config';
|
||||||
|
|
||||||
|
const iconOnly = ref(false);
|
||||||
|
|
||||||
|
const menu = computed(() => defaultStore.state.menu);
|
||||||
|
const otherMenuItemIndicated = computed(() => {
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (menu.value.includes(def)) continue;
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calcViewState = () => {
|
||||||
|
iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
|
||||||
|
};
|
||||||
|
|
||||||
|
calcViewState();
|
||||||
|
|
||||||
|
window.addEventListener('resize', calcViewState);
|
||||||
|
|
||||||
|
watch(defaultStore.reactiveState.menuDisplay, () => {
|
||||||
|
calcViewState();
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAccountMenu(ev: MouseEvent) {
|
||||||
|
openAccountMenu_({
|
||||||
|
withExtraOperation: true,
|
||||||
|
}, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInstanceMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: instance.name ?? host,
|
||||||
|
type: 'label',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.instanceInfo,
|
||||||
|
icon: 'fas fa-info-circle',
|
||||||
|
to: '/about',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.customEmojis,
|
||||||
|
icon: 'fas fa-laugh',
|
||||||
|
to: '/about#emojis',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.federation,
|
||||||
|
icon: 'fas fa-globe',
|
||||||
|
to: '/about#federation',
|
||||||
|
}], ev.currentTarget ?? ev.target, {
|
||||||
|
align: 'left',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function more(ev: MouseEvent) {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {
|
||||||
|
src: ev.currentTarget ?? ev.target,
|
||||||
|
}, {
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mvcprjjd {
|
||||||
|
$nav-width: 250px;
|
||||||
|
$nav-icon-only-width: 86px;
|
||||||
|
|
||||||
|
flex: 0 0 $nav-width;
|
||||||
|
width: $nav-width;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: clip;
|
||||||
|
background: var(--navBg);
|
||||||
|
contain: strict;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.iconOnly) {
|
||||||
|
> .body {
|
||||||
|
width: $nav-width;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 38px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 30px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 16px 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding-left: 30px;
|
||||||
|
line-height: 2.85rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--navFg);
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--navHoverFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--navActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 34px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.iconOnly {
|
||||||
|
flex: 0 0 $nav-icon-only-width;
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 52px;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 8px auto;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding: 18px 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 24px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon, > .text {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,209 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="kmwsukvl">
|
|
||||||
<div class="body">
|
|
||||||
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
|
||||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
|
||||||
</button>
|
|
||||||
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
|
|
||||||
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
|
||||||
</MkA>
|
|
||||||
<template v-for="item in menu">
|
|
||||||
<div v-if="item === '-'" class="divider"></div>
|
|
||||||
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
|
|
||||||
<i class="icon fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
|
||||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
|
||||||
</component>
|
|
||||||
</template>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
|
|
||||||
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
|
|
||||||
</MkA>
|
|
||||||
<button v-click-anime class="item _button" @click="more">
|
|
||||||
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
|
||||||
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
|
||||||
</button>
|
|
||||||
<MkA v-click-anime class="item" active-class="active" to="/settings">
|
|
||||||
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
|
||||||
</MkA>
|
|
||||||
<button class="item _button post" data-cy-open-post-form @click="post">
|
|
||||||
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineAsyncComponent, defineComponent, ref, toRef, watch } from 'vue';
|
|
||||||
import { host } from '@/config';
|
|
||||||
import { search } from '@/scripts/search';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { menuDef } from '@/menu';
|
|
||||||
import { openAccountMenu } from '@/account';
|
|
||||||
import { defaultStore } from '@/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
setup(props, context) {
|
|
||||||
const menu = toRef(defaultStore.state, 'menu');
|
|
||||||
const otherMenuItemIndicated = computed(() => {
|
|
||||||
for (const def in menuDef) {
|
|
||||||
if (menu.value.includes(def)) continue;
|
|
||||||
if (menuDef[def].indicated) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
host: host,
|
|
||||||
accounts: [],
|
|
||||||
connection: null,
|
|
||||||
menu,
|
|
||||||
menuDef: menuDef,
|
|
||||||
otherMenuItemIndicated,
|
|
||||||
post: os.post,
|
|
||||||
search,
|
|
||||||
openAccountMenu: (ev) => {
|
|
||||||
openAccountMenu({
|
|
||||||
withExtraOperation: true,
|
|
||||||
}, ev);
|
|
||||||
},
|
|
||||||
more: () => {
|
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {}, {
|
|
||||||
}, 'closed');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.kmwsukvl {
|
|
||||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
|
||||||
$avatar-size: 32px;
|
|
||||||
$avatar-margin: 8px;
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
|
|
||||||
> .divider {
|
|
||||||
margin: 16px 16px;
|
|
||||||
border-top: solid 0.5px var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .item {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
padding-left: 24px;
|
|
||||||
font-size: $ui-font-size;
|
|
||||||
line-height: 2.85rem;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--navFg);
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
position: relative;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon,
|
|
||||||
> .avatar {
|
|
||||||
margin-right: $avatar-margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
width: $avatar-size;
|
|
||||||
height: $avatar-size;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 20px;
|
|
||||||
color: var(--navIndicator);
|
|
||||||
font-size: 8px;
|
|
||||||
animation: blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .text {
|
|
||||||
position: relative;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--navHoverFg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: var(--navActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: calc(100% - 24px);
|
|
||||||
height: 100%;
|
|
||||||
margin: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--accentedBg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child, &:last-child {
|
|
||||||
position: sticky;
|
|
||||||
z-index: 1;
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
background: var(--X14);
|
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
&:before {
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
bottom: 0;
|
|
||||||
color: var(--fgOnAccent);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
height: calc(100% - 20px);
|
|
||||||
margin: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
&:before {
|
|
||||||
background: var(--accentLighten);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,303 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="mvcprjjd" :class="{ iconOnly }">
|
|
||||||
<div class="body">
|
|
||||||
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
|
||||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
|
||||||
</button>
|
|
||||||
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
|
|
||||||
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
|
||||||
</MkA>
|
|
||||||
<template v-for="item in menu">
|
|
||||||
<div v-if="item === '-'" class="divider"></div>
|
|
||||||
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
|
|
||||||
<i class="icon fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
|
||||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
|
||||||
</component>
|
|
||||||
</template>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
|
|
||||||
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
|
|
||||||
</MkA>
|
|
||||||
<button v-click-anime class="item _button" @click="more">
|
|
||||||
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
|
||||||
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
|
||||||
</button>
|
|
||||||
<MkA v-click-anime class="item" active-class="active" to="/settings">
|
|
||||||
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
|
||||||
</MkA>
|
|
||||||
<button class="item _button post" data-cy-open-post-form @click="os.post">
|
|
||||||
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { menuDef } from '@/menu';
|
|
||||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
|
||||||
import { defaultStore } from '@/store';
|
|
||||||
|
|
||||||
const iconOnly = ref(false);
|
|
||||||
|
|
||||||
const menu = computed(() => defaultStore.state.menu);
|
|
||||||
const otherMenuItemIndicated = computed(() => {
|
|
||||||
for (const def in menuDef) {
|
|
||||||
if (menu.value.includes(def)) continue;
|
|
||||||
if (menuDef[def].indicated) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const calcViewState = () => {
|
|
||||||
iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
|
|
||||||
};
|
|
||||||
|
|
||||||
calcViewState();
|
|
||||||
|
|
||||||
window.addEventListener('resize', calcViewState);
|
|
||||||
|
|
||||||
watch(defaultStore.reactiveState.menuDisplay, () => {
|
|
||||||
calcViewState();
|
|
||||||
});
|
|
||||||
|
|
||||||
function openAccountMenu(ev: MouseEvent) {
|
|
||||||
openAccountMenu_({
|
|
||||||
withExtraOperation: true,
|
|
||||||
}, ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
function more(ev: MouseEvent) {
|
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {
|
|
||||||
src: ev.currentTarget ?? ev.target,
|
|
||||||
}, {
|
|
||||||
}, 'closed');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mvcprjjd {
|
|
||||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
|
||||||
$nav-width: 250px;
|
|
||||||
$nav-icon-only-width: 86px;
|
|
||||||
$avatar-size: 32px;
|
|
||||||
$avatar-margin: 8px;
|
|
||||||
|
|
||||||
flex: 0 0 $nav-width;
|
|
||||||
width: $nav-width;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1001;
|
|
||||||
width: $nav-width;
|
|
||||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: auto;
|
|
||||||
overflow-x: clip;
|
|
||||||
background: var(--navBg);
|
|
||||||
contain: strict;
|
|
||||||
|
|
||||||
> .divider {
|
|
||||||
margin: 16px 16px;
|
|
||||||
border-top: solid 0.5px var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .item {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
padding-left: 24px;
|
|
||||||
font-size: $ui-font-size;
|
|
||||||
line-height: 2.85rem;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--navFg);
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
position: relative;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon,
|
|
||||||
> .avatar {
|
|
||||||
margin-right: $avatar-margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
width: $avatar-size;
|
|
||||||
height: $avatar-size;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 20px;
|
|
||||||
color: var(--navIndicator);
|
|
||||||
font-size: 8px;
|
|
||||||
animation: blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .text {
|
|
||||||
position: relative;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--navHoverFg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: var(--navActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
color: var(--accent);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: calc(100% - 24px);
|
|
||||||
height: 100%;
|
|
||||||
margin: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--accentedBg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child, &:last-child {
|
|
||||||
position: sticky;
|
|
||||||
z-index: 1;
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
background: var(--X14);
|
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
&:before {
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
bottom: 0;
|
|
||||||
color: var(--fgOnAccent);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
height: calc(100% - 20px);
|
|
||||||
margin: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
&:before {
|
|
||||||
background: var(--accentLighten);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.iconOnly {
|
|
||||||
flex: 0 0 $nav-icon-only-width;
|
|
||||||
width: $nav-icon-only-width;
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
width: $nav-icon-only-width;
|
|
||||||
|
|
||||||
> .divider {
|
|
||||||
margin: 8px auto;
|
|
||||||
width: calc(100% - 32px);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .item {
|
|
||||||
padding-left: 0;
|
|
||||||
padding: 18px 0;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: $ui-font-size * 1.1;
|
|
||||||
line-height: initial;
|
|
||||||
|
|
||||||
> .icon,
|
|
||||||
> .avatar {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
> .icon, > .text {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
width: min-content;
|
|
||||||
height: 100%;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.post {
|
|
||||||
height: $nav-icon-only-width;
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.post:before {
|
|
||||||
width: calc(100% - 28px);
|
|
||||||
height: auto;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
border-radius: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -20,9 +20,11 @@ import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
|
||||||
import MarqueeText from '@/components/marquee.vue';
|
import MarqueeText from '@/components/marquee.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { useInterval } from '@/scripts/use-interval';
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import { shuffle } from '@/scripts/shuffle';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
url?: string;
|
url?: string;
|
||||||
|
shuffle?: boolean;
|
||||||
display?: 'marquee' | 'oneByOne';
|
display?: 'marquee' | 'oneByOne';
|
||||||
marqueeDuration?: number;
|
marqueeDuration?: number;
|
||||||
marqueeReverse?: boolean;
|
marqueeReverse?: boolean;
|
||||||
|
@ -37,6 +39,9 @@ let key = $ref(0);
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
|
fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
|
||||||
res.json().then(feed => {
|
res.json().then(feed => {
|
||||||
|
if (props.shuffle) {
|
||||||
|
shuffle(feed.items);
|
||||||
|
}
|
||||||
items.value = feed.items;
|
items.value = feed.items;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
key++;
|
key++;
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
}]"
|
}]"
|
||||||
>
|
>
|
||||||
<span class="name">{{ x.name }}</span>
|
<span class="name">{{ x.name }}</span>
|
||||||
<XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url"/>
|
<XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/>
|
||||||
<XFederation v-else-if="x.type === 'federation'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
|
<XFederation v-else-if="x.type === 'federation'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
|
||||||
<XUserList v-else-if="x.type === 'userList'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :user-list-id="x.props.userListId"/>
|
<XUserList v-else-if="x.type === 'userList'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :user-list-id="x.props.userListId"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,6 +28,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dlrsnxqu {
|
.dlrsnxqu {
|
||||||
|
font-size: 15px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
|
|
||||||
> .item {
|
> .item {
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
</MkA>
|
</MkA>
|
||||||
<template v-for="item in menu">
|
<template v-for="item in menu">
|
||||||
<div v-if="item === '-'" class="divider"></div>
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime v-tooltip="$ts[menuDef[item].title]" class="item _button" :class="item" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
|
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="$ts[navbarItemDef[item].title]" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||||
<i class="fa-fw" :class="menuDef[item].icon"></i>
|
<i class="fa-fw" :class="navbarItemDef[item].icon"></i>
|
||||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
@ -43,7 +43,7 @@ import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
import { host } from '@/config';
|
import { host } from '@/config';
|
||||||
import { search } from '@/scripts/search';
|
import { search } from '@/scripts/search';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { menuDef } from '@/menu';
|
import { navbarItemDef } from '@/navbar';
|
||||||
import { openAccountMenu } from '@/account';
|
import { openAccountMenu } from '@/account';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ export default defineComponent({
|
||||||
host: host,
|
host: host,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
connection: null,
|
connection: null,
|
||||||
menuDef: menuDef,
|
navbarItemDef: navbarItemDef,
|
||||||
settingsWindowed: false,
|
settingsWindowed: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -68,9 +68,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
otherNavItemIndicated(): boolean {
|
otherNavItemIndicated(): boolean {
|
||||||
for (const def in this.menuDef) {
|
for (const def in this.navbarItemDef) {
|
||||||
if (this.menu.includes(def)) continue;
|
if (this.menu.includes(def)) continue;
|
||||||
if (this.menuDef[def].indicated) return true;
|
if (this.navbarItemDef[def].indicated) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -113,7 +113,7 @@ export default defineComponent({
|
||||||
withExtraOperation: true,
|
withExtraOperation: true,
|
||||||
}, ev);
|
}, ev);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@
|
||||||
</MkA>
|
</MkA>
|
||||||
<template v-for="item in menu">
|
<template v-for="item in menu">
|
||||||
<div v-if="item === '-'" class="divider"></div>
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="item" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
|
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||||
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
<i class="fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
|
||||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
@ -45,7 +45,7 @@ import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
import { host } from '@/config';
|
import { host } from '@/config';
|
||||||
import { search } from '@/scripts/search';
|
import { search } from '@/scripts/search';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { menuDef } from '@/menu';
|
import { navbarItemDef } from '@/navbar';
|
||||||
import { openAccountMenu } from '@/account';
|
import { openAccountMenu } from '@/account';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||||
|
@ -62,7 +62,7 @@ export default defineComponent({
|
||||||
host: host,
|
host: host,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
connection: null,
|
connection: null,
|
||||||
menuDef: menuDef,
|
navbarItemDef: navbarItemDef,
|
||||||
iconOnly: false,
|
iconOnly: false,
|
||||||
settingsWindowed: false,
|
settingsWindowed: false,
|
||||||
};
|
};
|
||||||
|
@ -74,9 +74,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
otherNavItemIndicated(): boolean {
|
otherNavItemIndicated(): boolean {
|
||||||
for (const def in this.menuDef) {
|
for (const def in this.navbarItemDef) {
|
||||||
if (this.menu.includes(def)) continue;
|
if (this.menu.includes(def)) continue;
|
||||||
if (this.menuDef[def].indicated) return true;
|
if (this.navbarItemDef[def].indicated) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -131,7 +131,7 @@ export default defineComponent({
|
||||||
withExtraOperation: true,
|
withExtraOperation: true,
|
||||||
}, ev);
|
}, ev);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,6 @@ import XCommon from './_common_/common.vue';
|
||||||
import { instanceName } from '@/config';
|
import { instanceName } from '@/config';
|
||||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { menuDef } from '@/menu';
|
|
||||||
import { mainRouter } from '@/router';
|
import { mainRouter } from '@/router';
|
||||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
|
|
@ -69,12 +69,12 @@ import { v4 as uuid } from 'uuid';
|
||||||
import XCommon from './_common_/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
|
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
|
||||||
import DeckColumnCore from '@/ui/deck/column-core.vue';
|
import DeckColumnCore from '@/ui/deck/column-core.vue';
|
||||||
import XSidebar from '@/ui/_common_/sidebar.vue';
|
import XSidebar from '@/ui/_common_/navbar.vue';
|
||||||
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
|
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import { getScrollContainer } from '@/scripts/scroll';
|
import { getScrollContainer } from '@/scripts/scroll';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { menuDef } from '@/menu';
|
import { navbarItemDef } from '@/navbar';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { mainRouter } from '@/router';
|
import { mainRouter } from '@/router';
|
||||||
|
@ -105,8 +105,8 @@ const columns = deckStore.reactiveState.columns;
|
||||||
const layout = deckStore.reactiveState.layout;
|
const layout = deckStore.reactiveState.layout;
|
||||||
const menuIndicated = computed(() => {
|
const menuIndicated = computed(() => {
|
||||||
if ($i == null) return false;
|
if ($i == null) return false;
|
||||||
for (const def in menuDef) {
|
for (const def in navbarItemDef) {
|
||||||
if (menuDef[def].indicated) return true;
|
if (navbarItemDef[def].indicated) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
@ -359,9 +359,10 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
width: 240px;
|
width: 240px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
contain: strict;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
background: var(--bg);
|
background: var(--navBg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<slot name="action"></slot>
|
<slot name="action"></slot>
|
||||||
</div>
|
</div>
|
||||||
<span class="header"><slot name="header"></slot></span>
|
<span class="header"><slot name="header"></slot></span>
|
||||||
<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-cog"></i></button>
|
<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-ellipsis"></i></button>
|
||||||
</header>
|
</header>
|
||||||
<div v-show="active" ref="body">
|
<div v-show="active" ref="body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -361,7 +361,6 @@ function onDrop(ev) {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: var(--deckColumnHeaderHeight);
|
width: var(--deckColumnHeaderHeight);
|
||||||
line-height: var(--deckColumnHeaderHeight);
|
line-height: var(--deckColumnHeaderHeight);
|
||||||
font-size: 16px;
|
|
||||||
color: var(--faceTextButton);
|
color: var(--faceTextButton);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -61,17 +61,17 @@ import { defineAsyncComponent, provide, onMounted, computed, ref, watch, Compute
|
||||||
import XCommon from './_common_/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
import { instanceName } from '@/config';
|
import { instanceName } from '@/config';
|
||||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||||
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
|
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { menuDef } from '@/menu';
|
import { navbarItemDef } from '@/navbar';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { Router } from '@/nirax';
|
import { Router } from '@/nirax';
|
||||||
import { mainRouter } from '@/router';
|
import { mainRouter } from '@/router';
|
||||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||||
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
||||||
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/sidebar.vue'));
|
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
|
||||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||||
|
|
||||||
const DESKTOP_THRESHOLD = 1100;
|
const DESKTOP_THRESHOLD = 1100;
|
||||||
|
@ -97,9 +97,9 @@ provideMetadataReceiver((info) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuIndicated = computed(() => {
|
const menuIndicated = computed(() => {
|
||||||
for (const def in menuDef) {
|
for (const def in navbarItemDef) {
|
||||||
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
||||||
if (menuDef[def].indicated) return true;
|
if (navbarItemDef[def].indicated) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
@ -365,11 +365,11 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
width: 240px;
|
width: 240px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
contain: strict;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
background: var(--bg);
|
background: var(--navBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { GetFormResultType } from '@/scripts/form';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import MkContainer from '@/components/ui/container.vue';
|
import MkContainer from '@/components/ui/container.vue';
|
||||||
import { useInterval } from '@/scripts/use-interval';
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import { shuffle } from '@/scripts/shuffle';
|
||||||
|
|
||||||
const name = 'rssTicker';
|
const name = 'rssTicker';
|
||||||
|
|
||||||
|
@ -34,6 +35,10 @@ const widgetPropsDef = {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
||||||
},
|
},
|
||||||
|
shuffle: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
refreshIntervalSec: {
|
refreshIntervalSec: {
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
default: 60,
|
default: 60,
|
||||||
|
@ -80,6 +85,9 @@ let key = $ref(0);
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
|
fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
|
||||||
res.json().then(feed => {
|
res.json().then(feed => {
|
||||||
|
if (widgetProps.shuffle) {
|
||||||
|
shuffle(feed.items);
|
||||||
|
}
|
||||||
items.value = feed.items;
|
items.value = feed.items;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
key++;
|
key++;
|
||||||
|
|
|
@ -6,33 +6,33 @@ import { createFilter, dataToEsm } from '@rollup/pluginutils';
|
||||||
import { RollupJsonOptions } from '@rollup/plugin-json';
|
import { RollupJsonOptions } from '@rollup/plugin-json';
|
||||||
|
|
||||||
export default function json5(options: RollupJsonOptions = {}): Plugin {
|
export default function json5(options: RollupJsonOptions = {}): Plugin {
|
||||||
const filter = createFilter(options.include, options.exclude);
|
const filter = createFilter(options.include, options.exclude);
|
||||||
const indent = 'indent' in options ? options.indent : '\t';
|
const indent = 'indent' in options ? options.indent : '\t';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'json5',
|
name: 'json5',
|
||||||
|
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
transform(json, id) {
|
transform(json, id) {
|
||||||
if (id.slice(-6) !== '.json5' || !filter(id)) return null;
|
if (id.slice(-6) !== '.json5' || !filter(id)) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON5.parse(json);
|
const parsed = JSON5.parse(json);
|
||||||
return {
|
return {
|
||||||
code: dataToEsm(parsed, {
|
code: dataToEsm(parsed, {
|
||||||
preferConst: options.preferConst,
|
preferConst: options.preferConst,
|
||||||
compact: options.compact,
|
compact: options.compact,
|
||||||
namedExports: options.namedExports,
|
namedExports: options.namedExports,
|
||||||
indent
|
indent,
|
||||||
}),
|
}),
|
||||||
map: { mappings: '' }
|
map: { mappings: '' },
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = 'Could not parse JSON file';
|
const message = 'Could not parse JSON file';
|
||||||
const position = parseInt(/[\d]/.exec(err.message)[0], 10);
|
const position = parseInt(/[\d]/.exec(err.message)[0], 10);
|
||||||
this.warn({ message, id, position });
|
this.warn({ message, id, position });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue