Compare commits

...

49 Commits

Author SHA1 Message Date
syuilo
ff24811676 Merge branch 'develop' 2022-07-16 18:21:44 +09:00
syuilo
ab3bc4a982 12.115.0 2022-07-16 18:21:29 +09:00
syuilo
b6e4ec7056 New Crowdin updates (#9004)
* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

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

* New translations ja-JP.yml (Chinese Simplified)
2022-07-16 18:19:38 +09:00
Kainoa Kanter
5619a3390d chore: improve ad style (#8995)
* Improve ad style

* 🎨

* `ad` -> `info`
2022-07-16 17:50:12 +09:00
syuilo
e9fd064624 chore(client): tweak user-info routing 2022-07-16 16:52:12 +09:00
syuilo
274bfc965f 12.115.0-beta.1 2022-07-16 15:56:27 +09:00
syuilo
672ceb8687 chore(client): tweak style 2022-07-16 15:19:52 +09:00
syuilo
a3a9b7fbd3 chore(client): tweak deck ui 2022-07-16 15:19:44 +09:00
syuilo
57e533a5ef chore(client): tweak ui 2022-07-16 13:49:23 +09:00
syuilo
4f9b03a997 refactor(client): use setup syntax 2022-07-16 13:14:16 +09:00
syuilo
01d07edfe3 chore(client): tweak style 2022-07-16 13:13:58 +09:00
syuilo
4c8a1867f0 Merge branch 'develop' 2022-07-15 22:45:13 +09:00
syuilo
7d63118941 12.114.0 2022-07-15 22:44:53 +09:00
syuilo
aa5eab746a New Crowdin updates (#9001)
* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Kabyle)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)
2022-07-15 22:43:42 +09:00
syuilo
3d0870f414 12.114.0-beta.8 2022-07-15 22:10:02 +09:00
syuilo
92e8a5dbd6 chore(client): tweak ui 2022-07-15 22:09:05 +09:00
syuilo
113df68843 chore(client): tweak style 2022-07-15 19:31:04 +09:00
syuilo
92792719bd chore(client): tweak style 2022-07-15 19:15:23 +09:00
syuilo
47186c0fff 12.114.0-beta.7 2022-07-15 17:14:41 +09:00
syuilo
eb73a8137d Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2022-07-15 17:14:29 +09:00
Johann150
7cb5b5c8c2 refactor: signup component as composition api (#8957) 2022-07-15 17:14:05 +09:00
syuilo
6f45208ab6 enhance(client): RSSティッカーで表示順序をシャッフルできるように 2022-07-15 17:12:08 +09:00
syuilo
165c4b2c00 chore(client): tweak ui 2022-07-15 17:01:13 +09:00
syuilo
a4b5a0072d move rollup to devDeps 2022-07-14 23:54:52 +09:00
syuilo
ddc899938a chore(client): improve usability 2022-07-14 23:50:07 +09:00
tamaina
77c2a7cd71 refactor(client): remove useCssModule (#8999)
* refactor(client): remove useCssModule()

* use MkStickyContainer

* Revert "use MkStickyContainer"

This reverts commit 639746786b.
2022-07-14 23:32:00 +09:00
tamaina
de24131993 use MkStickyContainer (#9000) 2022-07-14 23:31:01 +09:00
syuilo
7b7fe019c0 Update CHANGELOG.md 2022-07-14 23:24:10 +09:00
syuilo
c30ffec1af 12.114.0-beta.6 2022-07-14 22:26:02 +09:00
syuilo
44f560b453 tweak boot.js 2022-07-14 22:25:57 +09:00
syuilo
5fa8c62305 12.114.0-beta.5 2022-07-14 22:15:08 +09:00
syuilo
17500fc9c9 Revert "fix(client): revert es2017"
This reverts commit 0ddabdbf68.
2022-07-14 22:14:42 +09:00
syuilo
e3bad795e0 Revert "debug"
This reverts commit fa5140310f.
2022-07-14 22:13:04 +09:00
syuilo
1ba559a98b Revert "debug"
This reverts commit 800bbc4328.
2022-07-14 22:13:00 +09:00
syuilo
3053767c71 12.114.0-beta.4 2022-07-14 22:05:03 +09:00
syuilo
f2e91f4d62 tweak boot.js 2022-07-14 22:04:16 +09:00
syuilo
10f4815d34 tweak boot.js 2022-07-14 21:52:58 +09:00
syuilo
d43eb123b1 12.114.0-beta.3 2022-07-14 21:40:55 +09:00
syuilo
800bbc4328 debug 2022-07-14 21:40:30 +09:00
syuilo
7a43cac6b3 12.114.0-beta.2 2022-07-14 21:32:33 +09:00
syuilo
fa5140310f debug 2022-07-14 21:32:21 +09:00
syuilo
1dec3461cd 12.114.0-beta.1 2022-07-14 21:06:38 +09:00
syuilo
0ddabdbf68 fix(client): revert es2017 2022-07-14 21:06:07 +09:00
syuilo
d78faf1134 chore: use tab 2022-07-14 21:02:45 +09:00
syuilo
cb00786f1e improve error handling of client boot 2022-07-14 20:41:17 +09:00
syuilo
61e26696aa log error while client boot 2022-07-14 20:29:45 +09:00
syuilo
f3b0c6f1e7 chore(client): tweak ui 2022-07-14 17:52:59 +09:00
syuilo
242538ddce refactor(client): rename menu(sidebar) -> navbar 2022-07-14 17:42:12 +09:00
syuilo
64be9baed0 chore(client): tweak style 2022-07-14 17:22:06 +09:00
68 changed files with 1440 additions and 1063 deletions

View File

@@ -9,6 +9,20 @@
You should also include the user name that made the change.
-->
## 12.115.0 (2022/07/16)
### Improvements
- Client: Deckのプロファイル切り替えを簡単に @syuilo
- Client: UIのブラッシュアップ @syuilo
## 12.114.0 (2022/07/15)
### Improvements
- RSSティッカーで表示順序をシャッフルできるように @syuilo
### Bugfixes
- クライアントが起動しなくなることがある問題を修正 @syuilo
## 12.113.0 (2022/07/13)
### Improvements

View File

@@ -808,6 +808,7 @@ reverse: "اقلب"
colored: "ملوّن"
label: "التسمية"
localOnly: "المحلي فقط"
account: "الحسابات"
_emailUnavailable:
used: "هذا البريد الإلكتروني مستخدم"
format: "صيغة البريد الإلكتروني غير صالحة"

View File

@@ -848,6 +848,7 @@ reverse: "উল্টান"
colored: "রঙ্গিন"
label: "লেবেল"
localOnly: "শুধুমাত্র লোকাল"
account: "অ্যাকাউন্টগুলি"
_emailUnavailable:
used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে"
format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি"

View File

@@ -887,6 +887,9 @@ beta: "Beta"
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."
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: "Benutzerkonto"
_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."
sensitivity: "Erkennungssensitivität"

View File

@@ -887,6 +887,9 @@ beta: "Beta"
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."
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: "Account"
_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."
sensitivity: "Detection sensitivity"

View File

@@ -865,6 +865,7 @@ reverse: "Echar de un capirotazo"
colored: "Color"
label: "Etiqueta"
localOnly: "Solo local"
account: "Cuentas"
_emailUnavailable:
used: "Ya fue usado"
format: "Formato no válido."

View File

@@ -843,6 +843,7 @@ reverse: "Inverser"
colored: "Coloré"
label: "Étiquette"
localOnly: "Local seulement"
account: "Comptes"
_emailUnavailable:
used: "Non disponible"
format: "Le format de cette adresse de courriel est invalide"

View File

@@ -852,6 +852,7 @@ reverse: "Balik"
colored: "Diwarnai"
label: "Label"
localOnly: "Hanya lokal"
account: "Akun"
_emailUnavailable:
used: "Alamat surel ini telah digunakan"
format: "Format tidak valid."

View File

@@ -814,6 +814,7 @@ reverse: "Inverti"
colored: "Colorato"
label: "Etichetta"
localOnly: "Soltanto locale"
account: "Account"
_emailUnavailable:
used: "Email già in uso"
format: "Formato email non valido"

View File

@@ -887,6 +887,9 @@ beta: "ベータ"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、インスタンスによっては自動で設定されることがあります。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
navbar: "ナビゲーションバー"
shuffle: "シャッフル"
account: "アカウント"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1759,6 +1762,8 @@ _deck:
stackLeft: "左に重ねる"
popRight: "右に出す"
profile: "プロファイル"
newProfile: "新規プロファイル"
deleteProfile: "プロファイルを削除"
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"

View File

@@ -57,6 +57,7 @@ selectAccount: "Fren amiḍan"
accounts: "Imiḍan"
searchByGoogle: "Nadi"
file: "Ifuyla"
account: "Imiḍan"
_email:
_follow:
title: "Yeṭṭafaṛ-ik·em-id"

View File

@@ -886,6 +886,7 @@ beta: "베타"
enableAutoSensitive: "자동 NSFW 탐지"
enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다."
activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다."
account: "계정"
_sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
sensitivity: "탐지 민감도"

View File

@@ -764,6 +764,7 @@ file: "Pliki"
reverse: "Odwróć"
colored: "Kolorowe"
label: "Etykieta"
account: "Konta"
_ffVisibility:
public: "Publikuj"
_ad:

View File

@@ -842,6 +842,7 @@ reverse: "Переворот"
colored: "Выделена цветом"
label: "Метка"
localOnly: "Локально"
account: "Учётные записи"
_sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
setSensitiveFlagAutomatically: "Установить флаг NSFW"

View File

@@ -883,6 +883,8 @@ beta: "Beta"
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."
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:
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"

View File

@@ -820,8 +820,23 @@ ffVisibilityDescription: "ช่วยให้คุณสามารถกำ
continueThread: "ดูความต่อเนื่องเธรด"
deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?"
incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?"
hide: "ซ่อน"
leaveGroup: "ออกจากกลุ่ม"
leaveGroupConfirm: "คุณแน่ใจหรอว่าต้องการออกจาก \"{name}\""
useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ"
welcomeBackWithName: "ยินดีต้อนรับการกลับมานะค่ะ, {name}"
clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ"
overridedDeviceKind: "ประเภทอุปกรณ์"
smartphone: "สมาร์ทโฟน"
tablet: "แท็บเล็ต"
auto: "อัตโนมัติ"
themeColor: "อินสแตนซ์ Ticker Color"
size: "ขนาด"
numberOfColumn: "จำนวนคอลัมน์"
searchByGoogle: "ค้นหา"
file: "ไฟล์"
account: "บัญชีผู้ใช้"
_ffVisibility:
public: "เผยแพร่"
_ad:

View File

@@ -887,6 +887,9 @@ beta: "Beta"
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ủ."
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:
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"

View File

@@ -203,6 +203,7 @@ done: "完成"
processing: "正在处理"
preview: "预览"
default: "默认"
defaultValueIs: "默认值: {value}"
noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务"
federating: "联合中"
@@ -336,7 +337,7 @@ pinnedUsers: "置顶用户"
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。"
pinnedClipId: "置顶的签ID"
pinnedClipId: "置顶的便签ID"
pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha"
@@ -640,10 +641,12 @@ random: "随机"
system: "系统"
switchUi: "切换界面"
desktop: "桌面"
clip: "签"
clip: "便签"
createNew: "新建"
optional: "可选"
createNewClip: "新建签"
createNewClip: "新建便签"
unclip: "移除便签"
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签\"{name}\"里。您想要将本帖从该便签中移除吗?"
public: "公开"
i18nInfo: "Misskey已经被志愿者们翻译成了各种语言。如果你也有兴趣可以通过{link}帮助翻译。"
manageAccessTokens: "管理 Access Tokens"
@@ -677,7 +680,7 @@ pageLikesCount: "页面点赞次数"
pageLikedCount: "页面被点赞次数"
contact: "联系人"
useSystemFont: "使用系统默认字体"
clips: "签"
clips: "便签"
experimentalFeatures: "实验性功能"
developer: "开发者"
makeExplorable: "使账号可见。"
@@ -857,6 +860,7 @@ requireAdminForView: "需要使用管理员账户登录才能查看。"
isSystemAccount: "该账号由系统自动创建和管理。"
typeToConfirm: "输入 {x} 以确认操作。"
deleteAccount: "删除账户"
document: "文档"
numberOfPageCache: "缓存页数"
numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。"
logoutConfirm: "是否确认登出?"
@@ -867,6 +871,7 @@ reverse: "翻转"
colored: "彩色"
refreshInterval: "刷新间隔"
label: "标签"
type: "类型"
speed: "速度"
slow: "慢"
fast: "快"
@@ -878,6 +883,9 @@ cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
beta: "测试"
enableAutoSensitive: "自动 NSFW 识别"
navbar: "导航栏"
shuffle: "随机"
account: "账户"
_sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度"

View File

@@ -887,6 +887,8 @@ beta: "Beta"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "如果可用,請利用機器學習在媒體上自動設置 NSFW 旗標。 即使關閉此功能,依實例而定也可能會自動設置。"
activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,判斷它是否為免洗地址,或者它是否可以通信。 若關閉,則只會檢查字元是否正確。"
navbar: "導覽列"
account: "帳戶"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "12.113.0",
"version": "12.115.0",
"codename": "indigo",
"repository": {
"type": "git",

View File

@@ -14,9 +14,11 @@
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => {
window.onerror = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED', e);
};
window.onunhandledrejection = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
};
@@ -47,18 +49,30 @@
localStorage.setItem('localeVersion', v);
} else {
await checkUpdate();
renderError('LOCALE_FETCH_FAILED');
renderError('LOCALE_FETCH');
return;
}
}
//#endregion
//#region Script
import(`/assets/${CLIENT_ENTRY}`)
.catch(async e => {
await checkUpdate();
renderError('APP_FETCH_FAILED', e);
})
function importAppScript() {
import(`/assets/${CLIENT_ENTRY}`)
.catch(async e => {
await checkUpdate();
console.error(e);
renderError('APP_IMPORT', e);
});
}
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
if (document.readyState !== 'loading') {
importAppScript();
} else {
window.addEventListener('DOMContentLoaded', () => {
importAppScript();
});
}
//#endregion
//#region Theme
@@ -112,35 +126,35 @@
let errorsElement = document.getElementById('errors');
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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></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 stroke="none" d="M0 0h24v24H0z" fill="none"></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>
</svg>
<h1>An error has occurred!</h1>
<button class="button-big" onclick="location.reload(true);">
<span class="button-label-big">Refresh</span>
</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>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">Clear preferences and cache</span>
</button>
</a>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">Clear preferences and cache</span>
</button>
</a>
<br>
<a href="/cli">
<button class="button-small">
<span class="button-label-small">Start the simple client</span>
</button>
</a>
<a href="/cli">
<button class="button-small">
<span class="button-label-small">Start the simple client</span>
</button>
</a>
<br>
<a href="/bios">
<button class="button-small">
<span class="button-label-small">Start the repair tool</span>
</button>
</a>
<a href="/bios">
<button class="button-small">
<span class="button-label-small">Start the repair tool</span>
</button>
</a>
<br>
<div id="errors"></div>
`;
@@ -269,17 +283,22 @@
// eslint-disable-next-line no-inner-declarations
async function checkUpdate() {
// TODO: サーバーが落ちている場合などのエラーハンドリング
const res = await fetch('/api/meta', {
method: 'POST',
cache: 'no-cache'
});
try {
const res = await fetch('/api/meta', {
method: 'POST',
cache: 'no-cache'
});
const meta = await res.json();
const meta = await res.json();
if (meta.version != v) {
localStorage.setItem('v', meta.version);
refresh();
if (meta.version != v) {
localStorage.setItem('v', meta.version);
refresh();
}
} catch (e) {
console.error(e);
renderError('UPDATE_CHECK', e);
throw e;
}
}

View File

@@ -56,7 +56,6 @@
"random-seed": "0.3.0",
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
"rollup": "2.76.0",
"s-age": "1.1.2",
"sass": "1.53.0",
"seedrandom": "3.0.5",
@@ -102,6 +101,7 @@
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"rollup": "2.76.0",
"cross-env": "7.0.3",
"cypress": "10.3.0",
"eslint": "8.19.0",

View File

@@ -26,7 +26,8 @@
</div>
<button class="menu _button" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
</nav>
<div ref="main" class="main"
<div
ref="main" class="main"
:class="{ uploading: uploadings.length > 0, fetching }"
@dragover.prevent.stop="onDragover"
@dragenter="onDragenter"
@@ -142,7 +143,7 @@ const isDragSource = ref(false);
const fetching = ref(true);
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));
@@ -232,7 +233,7 @@ function onDrop(ev: DragEvent): any {
removeFile(file.id);
os.api('drive/files/update', {
fileId: file.id,
folderId: folder.value ? folder.value.id : null
folderId: folder.value ? folder.value.id : null,
});
}
//#endregion
@@ -248,7 +249,7 @@ function onDrop(ev: DragEvent): any {
removeFolder(droppedFolder.id);
os.api('drive/folders/update', {
folderId: droppedFolder.id,
parentId: folder.value ? folder.value.id : null
parentId: folder.value ? folder.value.id : null,
}).then(() => {
// noop
}).catch(err => {
@@ -256,13 +257,13 @@ function onDrop(ev: DragEvent): any {
case 'detected-circular-definition':
os.alert({
title: i18n.ts.unableToProcess,
text: i18n.ts.circularReferenceFolder
text: i18n.ts.circularReferenceFolder,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts.somethingHappened
text: i18n.ts.somethingHappened,
});
}
});
@@ -278,17 +279,17 @@ function urlUpload() {
os.inputText({
title: i18n.ts.uploadFromUrl,
type: 'url',
placeholder: i18n.ts.uploadFromUrlDescription
placeholder: i18n.ts.uploadFromUrlDescription,
}).then(({ canceled, result: url }) => {
if (canceled || !url) return;
os.api('drive/files/upload-from-url', {
url: url,
folderId: folder.value ? folder.value.id : undefined
folderId: folder.value ? folder.value.id : undefined,
});
os.alert({
title: i18n.ts.uploadFromUrlRequested,
text: i18n.ts.uploadFromUrlMayTakeTime
text: i18n.ts.uploadFromUrlMayTakeTime,
});
});
}
@@ -296,12 +297,12 @@ function urlUpload() {
function createFolder() {
os.inputText({
title: i18n.ts.createFolder,
placeholder: i18n.ts.folderName
placeholder: i18n.ts.folderName,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/create', {
name: name,
parentId: folder.value ? folder.value.id : undefined
parentId: folder.value ? folder.value.id : undefined,
}).then(createdFolder => {
addFolder(createdFolder, true);
});
@@ -312,12 +313,12 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
os.inputText({
title: i18n.ts.renameFolder,
placeholder: i18n.ts.inputNewFolderName,
default: folderToRename.name
default: folderToRename.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/update', {
folderId: folderToRename.id,
name: name
name: name,
}).then(updatedFolder => {
// FIXME: 画面を更新するために自分自身に移動
move(updatedFolder);
@@ -327,7 +328,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
os.api('drive/folders/delete', {
folderId: folderToDelete.id
folderId: folderToDelete.id,
}).then(() => {
// 削除時に親フォルダに移動
move(folderToDelete.parentId);
@@ -337,15 +338,15 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
os.alert({
type: 'error',
title: i18n.ts.unableToDelete,
text: i18n.ts.hasChildFilesOrFolders
text: i18n.ts.hasChildFilesOrFolders,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts.unableToDelete
text: i18n.ts.unableToDelete,
});
}
}
});
}
@@ -411,7 +412,7 @@ function move(target?: Misskey.entities.DriveFolder) {
fetching.value = true;
os.api('drive/folders/show', {
folderId: target
folderId: target,
}).then(folderToMove => {
folder.value = folderToMove;
hierarchyFolders.value = [];
@@ -510,7 +511,7 @@ async function fetch() {
const foldersPromise = os.api('drive/folders', {
folderId: folder.value ? folder.value.id : null,
limit: foldersMax + 1
limit: foldersMax + 1,
}).then(fetchedFolders => {
if (fetchedFolders.length === foldersMax + 1) {
moreFolders.value = true;
@@ -522,7 +523,7 @@ async function fetch() {
const filesPromise = os.api('drive/files', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
limit: filesMax + 1
limit: filesMax + 1,
}).then(fetchedFiles => {
if (fetchedFiles.length === filesMax + 1) {
moreFiles.value = true;
@@ -549,7 +550,7 @@ function fetchMoreFiles() {
folderId: folder.value ? folder.value.id : null,
type: props.type,
untilId: files.value[files.value.length - 1].id,
limit: max + 1
limit: max + 1,
}).then(files => {
if (files.length === max + 1) {
moreFiles.value = true;
@@ -569,30 +570,30 @@ function getMenu() {
ref: keepOriginal,
}, null, {
text: i18n.ts.addFile,
type: 'label'
type: 'label',
}, {
text: i18n.ts.upload,
icon: 'fas fa-upload',
action: () => { selectLocalFile(); }
action: () => { selectLocalFile(); },
}, {
text: i18n.ts.fromUrl,
icon: 'fas fa-link',
action: () => { urlUpload(); }
action: () => { urlUpload(); },
}, null, {
text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label'
type: 'label',
}, folder.value ? {
text: i18n.ts.renameFolder,
icon: 'fas fa-i-cursor',
action: () => { renameFolder(folder.value); }
action: () => { renameFolder(folder.value); },
} : undefined, folder.value ? {
text: i18n.ts.deleteFolder,
icon: 'fas fa-trash-alt',
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
} : undefined, {
text: i18n.ts.createFolder,
icon: 'fas fa-folder-plus',
action: () => { createFolder(); }
action: () => { createFolder(); },
}];
}
@@ -662,14 +663,14 @@ onBeforeUnmount(() => {
> .path {
display: inline-block;
vertical-align: bottom;
line-height: 50px;
line-height: 42px;
white-space: nowrap;
> * {
display: inline-block;
margin: 0;
padding: 0 8px;
line-height: 50px;
line-height: 42px;
cursor: pointer;
* {

View File

@@ -3,7 +3,7 @@
<div v-if="!showMenu" class="main" :class="ad.place">
<a :href="ad.url" target="_blank">
<img :src="ad.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle info-circle"></span></button>
</a>
</div>
<div v-else class="menu">
@@ -135,13 +135,19 @@ export default defineComponent({
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
> .menu {
position: absolute;
top: 0;
right: 0;
background: var(--panel);
top: 1px;
right: 1px;
> .info-circle {
border: 3px solid var(--panel);
border-radius: 50%;
background: var(--panel);
}
}
}

View File

@@ -1,68 +1,41 @@
char2filePath<template>
<template>
<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/>
<span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
<span v-else>{{ emoji }}</span>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue';
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { CustomEmoji } from 'misskey-js/built/entities';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { char2filePath } from '@/scripts/twemoji-base';
import { defaultStore } from '@/store';
import { instance } from '@/instance';
export default defineComponent({
props: {
emoji: {
type: String,
required: true
},
normal: {
type: Boolean,
required: false,
default: false
},
noStyle: {
type: Boolean,
required: false,
default: false
},
customEmojis: {
required: false
},
isReaction: {
type: Boolean,
default: false
},
},
const props = defineProps<{
emoji: string;
normal?: boolean;
noStyle?: boolean;
customEmojis?: CustomEmoji[];
isReaction?: boolean;
}>();
setup(props) {
const isCustom = computed(() => props.emoji.startsWith(':'));
const char = computed(() => isCustom.value ? null : props.emoji);
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
const ce = computed(() => props.customEmojis || instance.emojis || []);
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
const url = computed(() => {
if (char.value) {
return char2filePath(char.value);
} else {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url)
: customEmoji.value.url;
}
});
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
return {
url,
char,
alt,
customEmoji,
useOsNativeEmojis,
};
},
const isCustom = computed(() => props.emoji.startsWith(':'));
const char = computed(() => isCustom.value ? null : props.emoji);
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
const ce = computed(() => props.customEmojis ?? instance.emojis ?? []);
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
const url = computed(() => {
if (char.value) {
return char2filePath(char.value);
} else {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url)
: customEmoji.value.url;
}
});
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
</script>
<style lang="scss" scoped>

View File

@@ -16,9 +16,7 @@
</template>
<script lang="ts" setup>
import { useCssModule } from 'vue';
useCssModule();
import { } from 'vue';
const props = withDefaults(defineProps<{
inline?: boolean;

View File

@@ -18,7 +18,7 @@
</div>
</div>
<div v-if="!narrow || hideTitle" class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
@@ -27,7 +27,7 @@
</template>
<div class="buttons right">
<template v-for="action in actions">
<button v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</div>
</div>

View File

@@ -36,7 +36,7 @@
<script lang="ts" setup>
import { } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import { menuDef } from '@/menu';
import { navbarItemDef } from '@/navbar';
import { instanceName } from '@/config';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -62,7 +62,7 @@ const modal = $ref<InstanceType<typeof MkModal>>();
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',
text: i18n.ts[def.title],
icon: def.icon,

View File

@@ -16,7 +16,7 @@
<script lang="ts" setup>
import { toUnicode } from 'punycode';
import { useCssModule } from 'vue';
import { } from 'vue';
import tinycolor from 'tinycolor2';
import { host as localHost } from '@/config';
import { $i } from '@/account';
@@ -37,8 +37,6 @@ const isMe = $i && (
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
useCssModule();
</script>
<style lang="scss" scoped>

View File

@@ -554,6 +554,13 @@ function readPromo() {
&.max-width_500px {
font-size: 0.9em;
> .article {
> .avatar {
width: 50px;
height: 50px;
}
}
}
&.max-width_450px {
@@ -570,8 +577,8 @@ function readPromo() {
> .avatar {
margin: 0 10px 8px 0;
width: 50px;
height: 50px;
width: 46px;
height: 46px;
top: calc(14px + var(--stickyTop, 0px));
}
}
@@ -592,8 +599,6 @@ function readPromo() {
}
&.max-width_300px {
font-size: 0.825em;
> .article {
> .avatar {
width: 44px;

View File

@@ -177,13 +177,7 @@ useTooltip(reactionRef, (showing) => {
&.max-width_500px {
padding: 12px;
font-size: 0.8em;
}
&:after {
content: "";
display: block;
clear: both;
font-size: 0.85em;
}
> .head {

View File

@@ -1,255 +1,234 @@
<template>
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
<template v-if="meta">
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</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">
<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 #suffix>@{{ host }}</template>
<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-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 === '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 === '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>
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</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">
<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 #suffix>@{{ host }}</template>
<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-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 === '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 === '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>
</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>
</MkInput>
<MkInput v-if="meta.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="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>
</I18n>
</MkSwitch>
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</form>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import getPasswordStrength from 'syuilo-password-strength';
import { toUnicode } from 'punycode/';
import MkButton from './ui/button.vue';
import MkCaptcha from './captcha.vue';
import MkInput from './form/input.vue';
import MkSwitch from './form/switch.vue';
import { host, url } from '@/config';
import * as config from '@/config';
import * as os from '@/os';
import { login } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
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 props = withDefaults(defineProps<{
autoSet?: boolean;
}>(), {
autoSet: false,
});
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>
<style lang="scss" scoped>

View File

@@ -140,7 +140,7 @@ function focusDown() {
width: 100%;
box-sizing: border-box;
white-space: nowrap;
font-size: 0.85em;
font-size: 0.9em;
line-height: 20px;
text-align: left;
overflow: hidden;

View File

@@ -98,7 +98,7 @@ defineExpose({
}
> .header {
$height: 58px;
$height: 46px;
$height-narrow: 42px;
display: flex;
flex-shrink: 0;
@@ -138,6 +138,7 @@ defineExpose({
}
> .body {
flex: 1;
overflow: auto;
background: var(--panel);
}

View File

@@ -116,7 +116,7 @@ const setPosition = () => {
let top: number;
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);
} else {
left = props.x + props.innerMargin;

View File

@@ -393,7 +393,7 @@ export default defineComponent({
border-radius: var(--radius);
> .header {
--height: 45px;
--height: 42px;
&.mini {
--height: 38px;

View File

@@ -8,7 +8,6 @@ import tooltip from './tooltip';
import hotkey from './hotkey';
import appear from './appear';
import anim from './anim';
import stickyContainer from './sticky-container';
import clickAnime from './click-anime';
import panel from './panel';
import adaptiveBorder from './adaptive-border';
@@ -24,7 +23,6 @@ export default function(app: App) {
app.directive('appear', appear);
app.directive('anim', anim);
app.directive('click-anime', clickAnime);
app.directive('sticky-container', stickyContainer);
app.directive('panel', panel);
app.directive('adaptive-border', adaptiveBorder);
}

View File

@@ -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;

View File

@@ -7,10 +7,11 @@ import { popup, alert } from '@/os';
const start = isTouchUsing ? 'touchstart' : 'mouseover';
const end = isTouchUsing ? 'touchend' : 'mouseleave';
const delay = 100;
export default {
mounted(el: HTMLElement, binding, vn) {
const delay = binding.modifiers.noDelay ? 0 : 100;
const self = (el as any)._tooltipDirective_ = {} as any;
self.text = binding.value as string;

View File

@@ -6,7 +6,7 @@ import { i18n } from '@/i18n';
import { ui } from '@/config';
import { unisonReload } from '@/scripts/unison-reload';
export const menuDef = reactive({
export const navbarItemDef = reactive({
notifications: {
title: 'notifications',
icon: 'fas fa-bell',

View File

@@ -9,7 +9,7 @@
</div>
</div>
<div class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
@@ -20,7 +20,7 @@
<template v-if="actions">
<template v-for="action in actions">
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</template>
</div>

View File

@@ -9,8 +9,6 @@
<option value="left">{{ i18n.ts.left }}</option>
<option value="center">{{ i18n.ts.center }}</option>
</FormRadios>
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</div>
</template>
@@ -29,18 +27,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
const profile = computed(deckStore.makeGetterSetter('profile'));
async function setProfile() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
allowEmpty: false,
});
if (canceled) return;
profile.value = name;
unisonReload();
}
const headerActions = $computed(() => []);

View File

@@ -56,10 +56,10 @@
<FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ i18n.ts.fontSize }}</template>
<option value="small"><span style="font-size: 14px;">Aa</span></option>
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
<option value="large"><span style="font-size: 18px;">Aa</span></option>
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
<option :value="null"><span style="font-size: 14px;">Aa</span></option>
<option value="1"><span style="font-size: 15px;">Aa</span></option>
<option value="2"><span style="font-size: 16px;">Aa</span></option>
<option value="3"><span style="font-size: 17px;">Aa</span></option>
</FormRadios>
</FormSection>

View File

@@ -114,15 +114,15 @@ const menuDef = computed(() => [{
to: '/settings/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,
to: '/settings/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',
text: i18n.ts.sounds,
@@ -225,7 +225,7 @@ const component = computed(() => {
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
case 'theme/install': return defineAsyncComponent(() => import('./theme.install.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 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
@@ -291,6 +291,8 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(INFO);
// w 890
// h 700
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div class="_formRoot">
<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>
</FormTextarea>
@@ -23,7 +23,7 @@ import FormTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { navbarItemDef } from '@/navbar';
import { defaultStore } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
@@ -45,11 +45,11 @@ async function reloadAsk() {
}
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({
title: i18n.ts.addItem,
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,
}],
@@ -81,7 +81,7 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.menu,
title: i18n.ts.navbar,
icon: 'fas fa-list-ul',
});
</script>

View File

@@ -12,7 +12,7 @@
<FormSection>
<template #label>{{ i18n.ts.signinHistory }}</template>
<MkPagination :pagination="pagination">
<MkPagination :pagination="pagination" disable-auto-load>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">

View File

@@ -28,6 +28,9 @@
<MkInput v-model="statusbar.props.url" manual-save class="_formBlock" type="url">
<template #label>URL</template>
</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">
<template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput>
@@ -86,7 +89,6 @@ import FormRadios from '@/components/form/radios.vue';
import FormButton from '@/components/ui/button.vue';
import FormRange from '@/components/form/range.vue';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -101,6 +103,7 @@ watch(() => statusbar.type, () => {
if (statusbar.type === 'rss') {
statusbar.name = 'NEWS';
statusbar.props.url = 'http://feeds.afpbb.com/rss/afpbb/afpbbnews';
statusbar.props.shuffle = true;
statusbar.props.refreshIntervalSec = 120;
statusbar.props.display = 'marquee';
statusbar.props.marqueeDuration = 100;

View File

@@ -155,7 +155,7 @@ const age = $computed(() => {
});
function menu(ev) {
os.popupMenu(getUserMenu(props.user), ev.currentTarget ?? ev.target);
os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
}
function parallaxLoop() {

View File

@@ -1,12 +1,14 @@
<template>
<div v-sticky-container class="yrzkoczt">
<MkTab v-model="include" class="tab">
<option :value="null">{{ $ts.notes }}</option>
<option value="replies">{{ $ts.notesAndReplies }}</option>
<option value="files">{{ $ts.withFiles }}</option>
</MkTab>
<MkStickyContainer>
<template #header>
<MkTab v-model="include" :class="$style.tab">
<option :value="null">{{ $ts.notes }}</option>
<option value="replies">{{ $ts.notesAndReplies }}</option>
<option value="files">{{ $ts.withFiles }}</option>
</MkTab>
</template>
<XNotes :no-gap="true" :pagination="pagination"/>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -33,12 +35,10 @@ const pagination = {
};
</script>
<style lang="scss" scoped>
.yrzkoczt {
> .tab {
margin: calc(var(--margin) / 2) 0;
padding: calc(var(--margin) / 2) 0;
background: var(--bg);
}
<style lang="scss" module>
.tab {
margin: calc(var(--margin) / 2) 0;
padding: calc(var(--margin) / 2) 0;
background: var(--bg);
}
</style>

View File

@@ -23,7 +23,6 @@ import calcAge from 's-age';
import * as Acct from 'misskey-js/built/acct';
import * as misskey from 'misskey-js';
import { getScrollPosition } from '@/scripts/scroll';
import { getUserMenu } from '@/scripts/get-user-menu';
import number from '@/filters/number';
import { userPage, acct as getAcct } from '@/filters/user';
import * as os from '@/os';
@@ -65,10 +64,6 @@ watch(() => props.acct, fetchUser, {
immediate: true,
});
function menu(ev) {
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => user ? [{

View File

@@ -7,8 +7,9 @@ import * as os from '@/os';
import { userActions } from '@/store';
import { $i, iAmModerator } from '@/account';
import { mainRouter } from '@/router';
import { Router } from '@/nirax';
export function getUserMenu(user) {
export function getUserMenu(user, router: Router = mainRouter) {
const meId = $i ? $i.id : null;
async function pushList() {
@@ -161,7 +162,7 @@ export function getUserMenu(user) {
icon: 'fas fa-info-circle',
text: i18n.ts.info,
action: () => {
os.pageWindow(`/user-info/${user.id}`);
router.push(`/user-info/${user.id}`);
},
}, {
icon: 'fas fa-envelope',
@@ -227,7 +228,7 @@ export function getUserMenu(user) {
icon: 'fas fa-pencil-alt',
text: i18n.ts.editProfile,
action: () => {
mainRouter.push('/settings/profile');
router.push('/settings/profile');
},
}]);
}

View 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;
}

View File

@@ -30,7 +30,7 @@ html {
overflow: auto;
overflow-wrap: break-word;
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
font-size: 15px;
font-size: 14px;
line-height: 1.35;
text-size-adjust: 100%;
tab-size: 2;
@@ -61,16 +61,16 @@ html {
}
}
&.f-small {
font-size: 0.9em;
&.f-1 {
font-size: 15px;
}
&.f-large {
font-size: 1.1em;
&.f-2 {
font-size: 16px;
}
&.f-veryLarge {
font-size: 1.2em;
&.f-3 {
font-size: 17px;
}
&.useSystemFont {

View 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 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>

View 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.noDelay.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.noDelay.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.noDelay.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.noDelay.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.noDelay.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.noDelay.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.noDelay.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: 30px;
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -20,9 +20,11 @@ import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
import MarqueeText from '@/components/marquee.vue';
import * as os from '@/os';
import { useInterval } from '@/scripts/use-interval';
import { shuffle } from '@/scripts/shuffle';
const props = defineProps<{
url?: string;
shuffle?: boolean;
display?: 'marquee' | 'oneByOne';
marqueeDuration?: number;
marqueeReverse?: boolean;
@@ -37,6 +39,9 @@ let key = $ref(0);
const tick = () => {
fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
res.json().then(feed => {
if (props.shuffle) {
shuffle(feed.items);
}
items.value = feed.items;
fetching.value = false;
key++;

View File

@@ -10,7 +10,7 @@
}]"
>
<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"/>
<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>
@@ -28,6 +28,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
<style lang="scss" scoped>
.dlrsnxqu {
font-size: 15px;
background: var(--panel);
> .item {

View File

@@ -7,9 +7,9 @@
</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 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 } : {}">
<i class="fa-fw" :class="menuDef[item].icon"></i>
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
<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="navbarItemDef[item].icon"></i>
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
@@ -43,7 +43,7 @@ import { defineAsyncComponent, defineComponent } from 'vue';
import { host } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { navbarItemDef } from '@/navbar';
import { openAccountMenu } from '@/account';
import MkButton from '@/components/ui/button.vue';
@@ -57,7 +57,7 @@ export default defineComponent({
host: host,
accounts: [],
connection: null,
menuDef: menuDef,
navbarItemDef: navbarItemDef,
settingsWindowed: false,
};
},
@@ -68,9 +68,9 @@ export default defineComponent({
},
otherNavItemIndicated(): boolean {
for (const def in this.menuDef) {
for (const def in this.navbarItemDef) {
if (this.menu.includes(def)) continue;
if (this.menuDef[def].indicated) return true;
if (this.navbarItemDef[def].indicated) return true;
}
return false;
},
@@ -113,7 +113,7 @@ export default defineComponent({
withExtraOperation: true,
}, ev);
},
}
},
});
</script>

View File

@@ -14,9 +14,9 @@
</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-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
<i class="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="fas fa-circle"></i></span>
<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="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
@@ -45,7 +45,7 @@ import { defineAsyncComponent, defineComponent } from 'vue';
import { host } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { navbarItemDef } from '@/navbar';
import { openAccountMenu } from '@/account';
import MkButton from '@/components/ui/button.vue';
import { StickySidebar } from '@/scripts/sticky-sidebar';
@@ -62,7 +62,7 @@ export default defineComponent({
host: host,
accounts: [],
connection: null,
menuDef: menuDef,
navbarItemDef: navbarItemDef,
iconOnly: false,
settingsWindowed: false,
};
@@ -74,9 +74,9 @@ export default defineComponent({
},
otherNavItemIndicated(): boolean {
for (const def in this.menuDef) {
for (const def in this.navbarItemDef) {
if (this.menu.includes(def)) continue;
if (this.menuDef[def].indicated) return true;
if (this.navbarItemDef[def].indicated) return true;
}
return false;
},
@@ -131,7 +131,7 @@ export default defineComponent({
withExtraOperation: true,
}, ev);
},
}
},
});
</script>

View File

@@ -47,7 +47,6 @@ import XCommon from './_common_/common.vue';
import { instanceName } from '@/config';
import { StickySidebar } from '@/scripts/sticky-sidebar';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
import { defaultStore } from '@/store';

View File

@@ -33,8 +33,16 @@
<div>{{ i18n.ts._deck.introduction2 }}</div>
</div>
<div class="sideMenu">
<button v-tooltip.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
<button v-tooltip.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="fas fa-cog"></i></button>
<div class="top">
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" class="_button button" @click="changeProfile"><i class="fas fa-caret-down"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="fas fa-trash-can"></i></button>
</div>
<div class="middle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
</div>
<div class="bottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="fas fa-cog"></i></button>
</div>
</div>
</div>
</div>
@@ -67,17 +75,18 @@
import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store';
import DeckColumnCore from '@/ui/deck/column-core.vue';
import XSidebar from '@/ui/_common_/sidebar.vue';
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import MkButton from '@/components/ui/button.vue';
import { getScrollContainer } from '@/scripts/scroll';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { navbarItemDef } from '@/navbar';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { mainRouter } from '@/router';
import { unisonReload } from '@/scripts/unison-reload';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
mainRouter.navHook = (path): boolean => {
@@ -105,8 +114,8 @@ const columns = deckStore.reactiveState.columns;
const layout = deckStore.reactiveState.layout;
const menuIndicated = computed(() => {
if ($i == null) return false;
for (const def in menuDef) {
if (menuDef[def].indicated) return true;
for (const def in navbarItemDef) {
if (navbarItemDef[def].indicated) return true;
}
return false;
});
@@ -168,6 +177,51 @@ loadDeck();
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
// TODO??
}
function changeProfile(ev: MouseEvent) {
const items = ref([{
text: deckStore.state.profile,
active: true.valueOf,
}]);
getProfiles().then(profiles => {
items.value = [{
text: deckStore.state.profile,
active: true.valueOf,
}, ...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
text: k,
action: () => {
deckStore.set('profile', k);
unisonReload();
},
}))), null, {
text: i18n.ts._deck.newProfile,
icon: 'fas fa-plus',
action: async () => {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
allowEmpty: false,
});
if (canceled) return;
deckStore.set('profile', name);
unisonReload();
},
}];
});
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
async function deleteProfile() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('deleteAreYouSure', { x: deckStore.state.profile }),
});
if (canceled) return;
deleteProfile_(deckStore.state.profile);
deckStore.set('profile', 'default');
unisonReload();
}
</script>
<style lang="scss" scoped>
@@ -271,9 +325,25 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
justify-content: center;
width: 32px;
> .button {
width: 100%;
aspect-ratio: 1;
> .top, > .middle, > .bottom {
> .button {
display: block;
width: 100%;
aspect-ratio: 1;
}
}
> .top {
margin-bottom: auto;
}
> .middle {
margin-top: auto;
margin-bottom: auto;
}
> .bottom {
margin-top: auto;
}
}
}
@@ -359,9 +429,10 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
height: calc(var(--vh, 1vh) * 100);
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
background: var(--navBg);
}
}
</style>

View File

@@ -23,7 +23,7 @@
<slot name="action"></slot>
</div>
<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>
<div v-show="active" ref="body">
<slot></slot>
@@ -361,7 +361,6 @@ function onDrop(ev) {
z-index: 1;
width: var(--deckColumnHeaderHeight);
line-height: var(--deckColumnHeaderHeight);
font-size: 16px;
color: var(--faceTextButton);
&:hover {

View File

@@ -72,18 +72,8 @@ export const loadDeck = async () => {
return;
}
deckStore.set('columns', [{
id: 'a',
type: 'main',
name: i18n.ts._deck._columns.main,
width: 350,
}, {
id: 'b',
type: 'notifications',
name: i18n.ts._deck._columns.notifications,
width: 330,
}]);
deckStore.set('layout', [['a'], ['b']]);
deckStore.set('columns', []);
deckStore.set('layout', []);
return;
}
throw err;
@@ -105,6 +95,19 @@ export const saveDeck = throttle(1000, () => {
});
});
export async function getProfiles(): Promise<string[]> {
return await api('i/registry/keys', {
scope: ['client', 'deck', 'profiles'],
});
}
export async function deleteProfile(key: string): Promise<void> {
return await api('i/registry/remove', {
scope: ['client', 'deck', 'profiles'],
key: key,
});
}
export function addColumn(column: Column) {
if (column.name === undefined) column.name = null;
deckStore.push('columns', column);

View File

@@ -61,17 +61,17 @@ import { defineAsyncComponent, provide, onMounted, computed, ref, watch, Compute
import XCommon from './_common_/common.vue';
import { instanceName } from '@/config';
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 { defaultStore } from '@/store';
import { menuDef } from '@/menu';
import { navbarItemDef } from '@/navbar';
import { i18n } from '@/i18n';
import { $i } from '@/account';
import { Router } from '@/nirax';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
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 DESKTOP_THRESHOLD = 1100;
@@ -97,9 +97,9 @@ provideMetadataReceiver((info) => {
});
const menuIndicated = computed(() => {
for (const def in menuDef) {
for (const def in navbarItemDef) {
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
if (menuDef[def].indicated) return true;
if (navbarItemDef[def].indicated) return true;
}
return false;
});
@@ -365,11 +365,11 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
height: calc(var(--vh, 1vh) * 100);
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
background: var(--navBg);
}
}
</style>

View File

@@ -26,6 +26,7 @@ import { GetFormResultType } from '@/scripts/form';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
import { useInterval } from '@/scripts/use-interval';
import { shuffle } from '@/scripts/shuffle';
const name = 'rssTicker';
@@ -34,6 +35,10 @@ const widgetPropsDef = {
type: 'string' as const,
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
},
shuffle: {
type: 'boolean' as const,
default: true,
},
refreshIntervalSec: {
type: 'number' as const,
default: 60,
@@ -80,6 +85,9 @@ let key = $ref(0);
const tick = () => {
fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
res.json().then(feed => {
if (widgetProps.shuffle) {
shuffle(feed.items);
}
items.value = feed.items;
fetching.value = false;
key++;

View File

@@ -6,33 +6,33 @@ import { createFilter, dataToEsm } from '@rollup/pluginutils';
import { RollupJsonOptions } from '@rollup/plugin-json';
export default function json5(options: RollupJsonOptions = {}): Plugin {
const filter = createFilter(options.include, options.exclude);
const indent = 'indent' in options ? options.indent : '\t';
const filter = createFilter(options.include, options.exclude);
const indent = 'indent' in options ? options.indent : '\t';
return {
name: 'json5',
return {
name: 'json5',
// eslint-disable-next-line no-shadow
transform(json, id) {
if (id.slice(-6) !== '.json5' || !filter(id)) return null;
// eslint-disable-next-line no-shadow
transform(json, id) {
if (id.slice(-6) !== '.json5' || !filter(id)) return null;
try {
const parsed = JSON5.parse(json);
return {
code: dataToEsm(parsed, {
preferConst: options.preferConst,
compact: options.compact,
namedExports: options.namedExports,
indent
}),
map: { mappings: '' }
};
} catch (err) {
const message = 'Could not parse JSON file';
const position = parseInt(/[\d]/.exec(err.message)[0], 10);
this.warn({ message, id, position });
return null;
}
}
};
try {
const parsed = JSON5.parse(json);
return {
code: dataToEsm(parsed, {
preferConst: options.preferConst,
compact: options.compact,
namedExports: options.namedExports,
indent,
}),
map: { mappings: '' },
};
} catch (err) {
const message = 'Could not parse JSON file';
const position = parseInt(/[\d]/.exec(err.message)[0], 10);
this.warn({ message, id, position });
return null;
}
},
};
}