Compare commits

...

53 Commits

Author SHA1 Message Date
syuilo
09591fa4ae Merge branch 'develop' 2022-07-19 17:22:32 +09:00
syuilo
b495f6cfff 12.117.1 2022-07-19 17:22:24 +09:00
syuilo
c3c74c098d New translations ja-JP.yml (Slovak) (#9014) 2022-07-19 17:21:30 +09:00
syuilo
4769cd420b fix(client): 「インスタンスからのお知らせを受け取る」の設定を変更できない問題を修正
Fix #8474
2022-07-19 17:21:03 +09:00
syuilo
5b8d960b9f fix(client): tweak mfm-cheat-sheet 2022-07-19 17:13:24 +09:00
syuilo
7dd381bb5c Update CHANGELOG.md 2022-07-19 17:10:21 +09:00
Acid Chicken (硫酸鶏)
a3c2dbbfb5 fix(server): make sure getFileInfo doesn't fail if detectSensitivity ever fails (#9020) 2022-07-19 17:09:21 +09:00
syuilo
e35f599b6d update vite 2022-07-19 13:25:23 +09:00
syuilo
6fc42629c8 Update CHANGELOG.md 2022-07-19 13:23:36 +09:00
syuilo
a8867a8eea Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2022-07-19 01:20:38 +09:00
syuilo
264a6cda8e fix(client): fix window default prop 2022-07-19 01:20:36 +09:00
xianon
a675131b80 ユーザー情報の取得の再試行を修正する (#9017) 2022-07-18 21:43:31 +09:00
syuilo
369660ac79 chore(client): tweak style 2022-07-18 18:41:17 +09:00
syuilo
b3b0a960af update vite 2022-07-18 18:41:07 +09:00
syuilo
1c21cb4d82 lint fix 2022-07-18 05:08:13 +09:00
syuilo
85ce00adc0 Merge branch 'develop' 2022-07-18 05:07:12 +09:00
syuilo
036017a6af 12.117.0 2022-07-18 05:06:50 +09:00
syuilo
f095863b61 New Crowdin updates (#9009)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Bengali)

* 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 (German)

* New translations ja-JP.yml (Korean)

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

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)
2022-07-18 05:04:28 +09:00
syuilo
49499f3d7d enhance(client): tweak ui 2022-07-18 05:03:39 +09:00
syuilo
cbd07cdbe9 12.117.0-beta.1 2022-07-18 01:37:26 +09:00
syuilo
b123cbca58 chore(client): tweak style 2022-07-18 01:29:29 +09:00
Ryu jongheon
61d8b56eee fix(client): use icon for local if available (#9012) 2022-07-18 00:33:12 +09:00
syuilo
e9a97b4717 enhance(client): ウィンドウを最大化できるように 2022-07-18 00:31:55 +09:00
syuilo
01d7403dc4 🎨 2022-07-18 00:18:56 +09:00
syuilo
8f3ca867d2 enhance(client): tweak ui 2022-07-17 23:18:05 +09:00
syuilo
d7222dd56a enhance(client): tweak ui 2022-07-17 21:06:33 +09:00
syuilo
f25518af91 Merge branch 'develop' 2022-07-17 05:35:36 +09:00
syuilo
3b69a563f8 12.116.1 2022-07-17 05:35:28 +09:00
syuilo
0331f3c61b chore(client): tweak deck ui 2022-07-17 05:33:21 +09:00
syuilo
2dae56fc8f chore(client): tweak style 2022-07-17 05:13:07 +09:00
syuilo
bec2d9e3fa Update 1651224615271-foreign-key.js
#8852
2022-07-17 05:12:59 +09:00
syuilo
da92988fb8 fix(client): fix deck style 2022-07-17 05:12:43 +09:00
syuilo
77d4d6e377 fix(client): デッキUI時に ページで表示 ボタンが機能しない問題を修正
Fix #9010
2022-07-17 05:12:22 +09:00
syuilo
b796aacf7f Merge branch 'develop' 2022-07-16 23:53:24 +09:00
syuilo
a974ab00d7 12.116.0 2022-07-16 23:53:15 +09:00
syuilo
4352331b70 New Crowdin updates (#9007)
* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Chinese Traditional)
2022-07-16 23:53:01 +09:00
syuilo
e4453e9ca8 Update 1651224615271-foreign-key.js
#8852
2022-07-16 23:51:32 +09:00
syuilo
3811b90150 12.116.0-beta.1 2022-07-16 23:11:08 +09:00
syuilo
003f592ef6 feat(client): registry editor 2022-07-16 23:11:05 +09:00
syuilo
a3f3ef4226 chore(client): tweak style 2022-07-16 22:16:01 +09:00
syuilo
fbb0cc686e chore(client): tweak style 2022-07-16 20:53:53 +09:00
syuilo
c1c8c9c37c fix(server): fix bind of method
Fix #9006
2022-07-16 19:03:41 +09:00
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
70 changed files with 1853 additions and 1135 deletions

View File

@@ -9,6 +9,48 @@
You should also include the user name that made the change.
-->
## 12.117.1 (2022/07/19)
### Improvements
- Client: UIのブラッシュアップ @syuilo
### Bugfixes
- Server: ファイルのアップロードに失敗することがある問題を修正 @acid-chicken
- Client: リアクションピッカーがアプリ内ウィンドウの後ろに表示されてしまう問題を修正 @syuilo
- Client: ユーザー情報の取得の再試行を修正 @xianonn
- Client: MFMチートシートの挙動を修正 @syuilo
- Client: 「インスタンスからのお知らせを受け取る」の設定を変更できない問題を修正 @syuilo
## 12.117.0 (2022/07/18)
### Improvements
- Client: ウィンドウを最大化できるように @syuilo
- Client: Shiftキーを押した状態でリンクをクリックするとアプリ内ウィンドウで開くように @syuilo
- Client: デッキを使用している際、Ctrlキーを押した状態でリンクをクリックするとページ遷移を強制できるように @syuilo
- Client: UIのブラッシュアップ @syuilo
## 12.116.1 (2022/07/17)
### Bugfixes
- Client: デッキUI時に ページで表示 ボタンが機能しない問題を修正 @syuilo
- Error During Migration Run to 12.111.x
## 12.116.0 (2022/07/16)
### Improvements
- Client: registry editor @syuilo
- Client: UIのブラッシュアップ @syuilo
### Bugfixes
- Error During Migration Run to 12.111.x
- Server: TypeError: Cannot convert undefined or null to object @syuilo
## 12.115.0 (2022/07/16)
### Improvements
- Client: Deckのプロファイル切り替えを簡単に @syuilo
- Client: UIのブラッシュアップ @syuilo
## 12.114.0 (2022/07/15)
### Improvements

View File

@@ -1645,6 +1645,7 @@ _deck:
alwaysShowMainColumn: "সর্বদা মেইন কলাম দেখান"
columnAlign: "কলাম সাজান"
addColumn: "কলাম যুক্ত করুন"
configureColumn: "কলাম সেটিংস"
swapLeft: "বামে সরান"
swapRight: "ডানে সরান"
swapUp: "উপরে উঠান"

View File

@@ -889,7 +889,8 @@ enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Mach
activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft."
navbar: "Navigationsleiste"
shuffle: "Mischen"
account: "Benutzerkonten"
account: "Benutzerkonto"
move: "Verschieben"
_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"
@@ -1696,6 +1697,7 @@ _deck:
alwaysShowMainColumn: "Hauptspalte immer zeigen"
columnAlign: "Spaltenausrichtung"
addColumn: "Spalte hinzufügen"
configureColumn: "Spalteneinstellungen"
swapLeft: "Mit linker Spalte tauschen"
swapRight: "Mit rechter Spalte tauschen"
swapUp: "Mit oberer Spalte tauschen"
@@ -1703,6 +1705,8 @@ _deck:
stackLeft: "Auf linke Spalte stapeln"
popRight: "Nach rechts vom Stapel nehmen"
profile: "Profil"
newProfile: "Neues Profil"
deleteProfile: "Profil löschen"
introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!"
introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen."
widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu."

View File

@@ -889,7 +889,8 @@ enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW
activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated."
navbar: "Navigation bar"
shuffle: "Shuffle"
account: "Accounts"
account: "Account"
move: "Move"
_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"
@@ -1696,6 +1697,7 @@ _deck:
alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns"
addColumn: "Add column"
configureColumn: "Column settings"
swapLeft: "Swap with the left column"
swapRight: "Swap with the right column"
swapUp: "Swap with the above column"
@@ -1703,6 +1705,8 @@ _deck:
stackLeft: "Stack with the left column"
popRight: "Pop column to the right"
profile: "Profile"
newProfile: "New profile"
deleteProfile: "Delete profile"
introduction: "Create the perfect interface for you by arranging columns freely!"
introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."

View File

@@ -890,6 +890,7 @@ activeEmailValidationDescription: "ユーザーのメールアドレスのバリ
navbar: "ナビゲーションバー"
shuffle: "シャッフル"
account: "アカウント"
move: "移動"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1037,6 +1038,8 @@ _mfm:
sparkleDescription: "キラキラしたパーティクルのエフェクトを追加します。"
rotate: "回転"
rotateDescription: "指定した角度で回転させます。"
plain: "プレーン"
plainDescription: "内側の構文を全て無効にします。"
_instanceTicker:
none: "表示しない"
@@ -1755,6 +1758,7 @@ _deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
addColumn: "カラムを追加"
configureColumn: "カラムの設定"
swapLeft: "左に移動"
swapRight: "右に移動"
swapUp: "上に移動"
@@ -1762,6 +1766,8 @@ _deck:
stackLeft: "左に重ねる"
popRight: "右に出す"
profile: "プロファイル"
newProfile: "新規プロファイル"
deleteProfile: "プロファイルを削除"
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"

View File

@@ -886,6 +886,8 @@ beta: "베타"
enableAutoSensitive: "자동 NSFW 탐지"
enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다."
activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다."
navbar: "네비게이션 바"
shuffle: "셔플"
account: "계정"
_sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
@@ -1692,6 +1694,7 @@ _deck:
alwaysShowMainColumn: "메인 칼럼 항상 표시"
columnAlign: "칼럼 정렬"
addColumn: "칼럼 추가"
configureColumn: "칼럼 설정"
swapLeft: "왼쪽으로 이동"
swapRight: "오른쪽으로 이동"
swapUp: "위로 이동"
@@ -1699,6 +1702,8 @@ _deck:
stackLeft: "왼쪽에 쌓기"
popRight: "오른쪽으로 빼기"
profile: "프로파일"
newProfile: "새 프로파일"
deleteProfile: "프로파일 삭제"
introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!"
introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다."
widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요"

View File

@@ -842,6 +842,9 @@ reverse: "Переворот"
colored: "Выделена цветом"
label: "Метка"
localOnly: "Локально"
beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта."
account: "Учётные записи"
_sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
@@ -1634,6 +1637,7 @@ _deck:
alwaysShowMainColumn: "Всегда показывать главную колонку"
columnAlign: "Выравнивание колонок"
addColumn: "Добавить колонку"
configureColumn: "Настройки колонок"
swapLeft: "Переставить левее"
swapRight: "Переставить правее"
swapUp: "Переставить выше"

View File

@@ -885,6 +885,7 @@ enableAutoSensitiveDescription: "Ak je zapnuté, príznak NSFW sa na médiách a
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"
move: "Pohyb"
_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"
@@ -1691,6 +1692,7 @@ _deck:
alwaysShowMainColumn: "Vždy zobraziť v hlavnom stĺpci"
columnAlign: "Zarovnať stĺpce"
addColumn: "Pridať stĺpec"
configureColumn: "Nastavenie stĺpcov"
swapLeft: "Vymeniť vľavo"
swapRight: "Vymeniť vpravo"
swapUp: "Vymeniť hore"
@@ -1698,6 +1700,8 @@ _deck:
stackLeft: "Priložiť do ľavého stĺpca"
popRight: "Vybrať napravo"
profile: "Profil"
newProfile: "Nový profil"
deleteProfile: "Odstrániť profil"
introduction: "Kombinujte stĺpce a vytvorte si svoje vlastné rozhranie!"
introduction2: "Stlačením tlačidla + v pravej časti obrazovky môžete kedykoľvek pridať stĺpce."
widgetsIntroduction: "V ponuke stĺpca vyberte možnosť \"Upraviť widget\" a pridajte widget"

View File

@@ -820,6 +820,20 @@ 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: "บัญชีผู้ใช้"

View File

@@ -1696,6 +1696,7 @@ _deck:
alwaysShowMainColumn: "Luôn hiện cột chính"
columnAlign: "Căn cột"
addColumn: "Thêm cột"
configureColumn: "Cài đặt cột"
swapLeft: "Hoán đổi với cột bên trái"
swapRight: "Hoán đổi với cột bên phải"
swapUp: "Hoán đổi với cột trên"
@@ -1703,6 +1704,8 @@ _deck:
stackLeft: "Xếp chồng với cột bên trái"
popRight: "Xếp chồng với cột bên trái"
profile: "Hồ sơ"
newProfile: "Hồ sơ mới"
deleteProfile: "Xóa hồ sơ"
introduction: "Kết hợp các cột để tạo giao diện của riêng bạn!"
introduction2: "Bạn có thể thêm cột bất kỳ lúc nào bằng cách nhấn + ở bên phải màn hình."
widgetsIntroduction: "Chọn \"Sửa widget\" trong menu cột và thêm một widget."

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,8 +860,9 @@ requireAdminForView: "需要使用管理员账户登录才能查看。"
isSystemAccount: "该账号由系统自动创建和管理。"
typeToConfirm: "输入 {x} 以确认操作。"
deleteAccount: "删除账户"
document: "文档"
numberOfPageCache: "缓存页数"
numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。"
numberOfPageCacheDescription: "设置较高的值会更方便用户,但设备的负载和内存使用量会增加。"
logoutConfirm: "是否确认登出?"
lastActiveDate: "最后活跃时间"
statusbar: "状态栏"
@@ -867,6 +871,7 @@ reverse: "翻转"
colored: "彩色"
refreshInterval: "刷新间隔"
label: "标签"
type: "类型"
speed: "速度"
slow: "慢"
fast: "快"
@@ -878,6 +883,8 @@ cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
beta: "测试"
enableAutoSensitive: "自动 NSFW 识别"
navbar: "导航栏"
shuffle: "随机"
account: "账户"
_sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
@@ -1684,13 +1691,16 @@ _deck:
alwaysShowMainColumn: "总是显示主列"
columnAlign: "列对齐"
addColumn: "添加列"
configureColumn: "列设置"
swapLeft: "向左移动"
swapRight: "向右移动"
swapUp: "向上移动"
swapDown: "向下移动"
stackLeft: "向左折叠"
popRight: "向右弹出"
profile: "个人资料"
profile: "配置文件"
newProfile: "新建配置文件"
deleteProfile: "删除配置文件"
_columns:
main: "主列"
widgets: "小工具"

View File

@@ -155,7 +155,7 @@ searchWith: "搜尋: {q}"
youHaveNoLists: "你沒有任何清單"
followConfirm: "你真的要追隨{name}嗎?"
proxyAccount: "代理帳戶"
proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者關注該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶關注。"
proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者追蹤該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶追蹤。"
host: "主機"
selectUser: "選取使用者"
recipient: "收件人"
@@ -1695,6 +1695,7 @@ _deck:
alwaysShowMainColumn: "總是顯示主欄"
columnAlign: "對齊欄位"
addColumn: "新增欄位"
configureColumn: "欄位的設定"
swapLeft: "向左移動"
swapRight: "向右移動"
swapUp: "往上移動"
@@ -1702,6 +1703,8 @@ _deck:
stackLeft: "向左折疊"
popRight: "向右彈出"
profile: "個人檔案"
newProfile: "新建個人檔案"
deleteProfile: "刪除個人檔案"
introduction: "組合欄位來製作屬於自己的介面吧!"
introduction2: "您可以隨時透過按畫面右方的 + 來添加欄位。"
widgetsIntroduction: "請從欄位的選單中,選擇「編輯小工具」來添加小工具"

View File

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

View File

@@ -28,11 +28,11 @@ export class foreignKeyReports1651224615271 {
queryRunner.query(`CREATE INDEX "IDX_315c779174fe8247ab324f036e" ON "drive_file" ("isLink")`),
queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId")`),
queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`),
//queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`),
queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => {
queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}),
//queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => {
// queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
//}),
queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`),
queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`),

View File

@@ -101,13 +101,17 @@ export async function getFileInfo(path: string, opts: {
let porn = false;
if (!opts.skipSensitiveDetection) {
[sensitive, porn] = await detectSensitivity(
await detectSensitivity(
path,
type.mime,
opts.sensitiveThreshold ?? 0.5,
opts.sensitiveThresholdForPorn ?? 0.75,
opts.enableSensitiveMediaDetectionForVideos ?? false,
);
).then(value => {
[sensitive, porn] = value;
}, error => {
warnings.push(`detectSensitivity failed: ${error}`);
});
}
return {

View File

@@ -16,6 +16,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.emitTypers = this.emitTypers.bind(this);
}
public async init(params: any) {

View File

@@ -15,7 +15,7 @@
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"@syuilo/aiscript": "0.11.1",
"@vitejs/plugin-vue": "3.0.0",
"@vitejs/plugin-vue": "3.0.1",
"@vue/compiler-sfc": "3.2.37",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
@@ -73,7 +73,7 @@
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vite": "3.0.0",
"vite": "3.0.2",
"vue": "3.2.37",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "4.0.1",

View File

@@ -206,17 +206,16 @@ export async function openAccountMenu(opts: {
to: `/@${ $i.username }`,
avatar: $i,
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
type: 'parent',
icon: 'fas fa-plus',
text: i18n.ts.addAccount,
action: () => {
popupMenu([{
text: i18n.ts.existingAccount,
action: () => { showSigninDialog(); },
}, {
text: i18n.ts.createAccount,
action: () => { createAccount(); },
}], ev.currentTarget ?? ev.target);
},
children: [{
text: i18n.ts.existingAccount,
action: () => { showSigninDialog(); },
}, {
text: i18n.ts.createAccount,
action: () => { createAccount(); },
}],
}, {
type: 'link',
icon: 'fas fa-users',

View File

@@ -77,9 +77,9 @@ const inputEl = ref<HTMLElement>();
const prefixEl = ref<HTMLElement>();
const suffixEl = ref<HTMLElement>();
const height =
props.small ? 38 :
props.large ? 42 :
40;
props.small ? 36 :
props.large ? 40 :
38;
const focus = () => inputEl.value.focus();
const onInput = (ev: KeyboardEvent) => {

View File

@@ -63,9 +63,9 @@ const prefixEl = ref(null);
const suffixEl = ref(null);
const container = ref(null);
const height =
props.small ? 38 :
props.large ? 42 :
40;
props.small ? 36 :
props.large ? 40 :
38;
const focus = () => inputEl.value.focus();
const onInput = (ev) => {

View File

@@ -50,7 +50,7 @@ function onContextmenu(ev) {
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: () => {
router.push(props.to);
router.push(props.to, 'forcePage');
},
}, null, {
icon: 'fas fa-external-link-alt',
@@ -79,7 +79,7 @@ function popout() {
popout_(props.to);
}
function nav() {
function nav(ev: MouseEvent) {
if (props.behavior === 'browser') {
location.href = props.to;
return;
@@ -93,6 +93,10 @@ function nav() {
}
}
router.push(props.to);
if (ev.shiftKey) {
return openWindow();
}
router.push(props.to, ev.ctrlKey ? 'forcePage' : null);
}
</script>

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

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

@@ -8,6 +8,7 @@
<script lang="ts" setup>
import { } from 'vue';
import { instanceName } from '@/config';
import { instance as Instance } from '@/instance';
const props = defineProps<{
instance?: {
@@ -19,7 +20,7 @@ const props = defineProps<{
// if no instance data is given, this is for the local instance
const instance = props.instance ?? {
faviconUrl: '/favicon.ico',
faviconUrl: Instance.iconUrl || Instance.faviconUrl || '/favicon.ico',
name: instanceName,
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
};

View File

@@ -15,20 +15,6 @@
</MkA>
</template>
</div>
<div class="sub">
<button v-click-anime class="_button" @click="help">
<i class="fas fa-question-circle icon"></i>
<div class="text">{{ $ts.help }}</div>
</button>
<MkA v-click-anime to="/about" @click.passive="close()">
<i class="fas fa-info-circle icon"></i>
<div class="text">{{ $ts.instanceInfo }}</div>
</MkA>
<MkA v-click-anime to="/about-misskey" @click.passive="close()">
<img src="/static-assets/favicon.png" class="icon"/>
<div class="text">{{ $ts.aboutMisskey }}</div>
</MkA>
</div>
</div>
</MkModal>
</template>
@@ -74,28 +60,6 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
function close() {
modal.close();
}
function help(ev: MouseEvent) {
os.popupMenu([{
type: 'link',
to: '/mfm-cheat-sheet',
text: i18n.ts._mfm.cheatSheet,
icon: 'fas fa-code',
}, {
type: 'link',
to: '/scratchpad',
text: i18n.ts.scratchpad,
icon: 'fas fa-terminal',
}, null, {
text: i18n.ts.document,
icon: 'fas fa-question-circle',
action: () => {
window.open('https://misskey-hub.net/help.html', '_blank');
},
}], ev.currentTarget ?? ev.target);
close();
}
</script>
<style lang="scss" scoped>

View File

@@ -390,7 +390,7 @@ if (appearNote.replyId) {
> .article {
padding: 32px;
font-size: 1.1em;
font-size: 1.2em;
> .header {
display: flex;

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

View File

@@ -122,7 +122,7 @@ function close() {
}
function expand() {
mainRouter.push(router.getCurrentPath());
mainRouter.push(router.getCurrentPath(), 'forcePage');
windowEl.close();
}

View File

@@ -50,14 +50,14 @@ const emit = defineEmits<{
}
> .name {
font-size: 0.9em;
font-size: 1em;
}
}
> .users {
flex: 1;
min-width: 0;
font-size: 0.9em;
font-size: 0.95em;
border-left: solid 0.5px var(--divider);
padding-left: 10px;
margin-left: 10px;

View File

@@ -1,5 +1,6 @@
<template>
<button v-if="!link" class="bghgjjyj _button"
<button
v-if="!link" class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full }"
:type="type"
@click="$emit('click', $event)"
@@ -10,7 +11,8 @@
<slot></slot>
</div>
</button>
<MkA v-else class="bghgjjyj _button"
<MkA
v-else class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full }"
:to="to"
@mousedown="onMousedown"
@@ -29,56 +31,56 @@ export default defineComponent({
props: {
type: {
type: String,
required: false
required: false,
},
primary: {
type: Boolean,
required: false,
default: false
default: false,
},
gradate: {
type: Boolean,
required: false,
default: false
default: false,
},
rounded: {
type: Boolean,
required: false,
default: false
default: true,
},
inline: {
type: Boolean,
required: false,
default: false
default: false,
},
link: {
type: Boolean,
required: false,
default: false
default: false,
},
to: {
type: String,
required: false
required: false,
},
autofocus: {
type: Boolean,
required: false,
default: false
default: false,
},
wait: {
type: Boolean,
required: false,
default: false
default: false,
},
danger: {
type: Boolean,
required: false,
default: false
default: false,
},
full: {
type: Boolean,
required: false,
default: false
default: false,
},
},
emits: ['click'],
@@ -127,8 +129,8 @@ export default defineComponent({
window.setTimeout(() => {
if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
}, 2000);
}
}
},
},
});
</script>
@@ -142,8 +144,7 @@ export default defineComponent({
padding: 8px 14px;
text-align: center;
font-weight: normal;
font-size: 0.9em;
line-height: 22px;
font-size: 1em;
box-shadow: none;
text-decoration: none;
background: var(--buttonBg);

View File

@@ -1,16 +1,16 @@
<template>
<transition :name="$store.state.animation ? 'fade' : ''" appear>
<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
</div>
</transition>
</template>
<script lang="ts" setup>
import { onMounted, onBeforeUnmount } from 'vue';
import contains from '@/scripts/contains';
import MkMenu from './menu.vue';
import { MenuItem } from './types/menu.vue';
import contains from '@/scripts/contains';
import * as os from '@/os';
const props = defineProps<{

View File

@@ -0,0 +1,65 @@
<template>
<div ref="el" class="sfhdhdhr">
<MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
</div>
</template>
<script lang="ts" setup>
import { on } from 'events';
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
import MkMenu from './menu.vue';
import { MenuItem } from '@/types/menu';
import * as os from '@/os';
const props = defineProps<{
items: MenuItem[];
targetElement: HTMLElement;
rootElement: HTMLElement;
width?: number;
viaKeyboard?: boolean;
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
(ev: 'actioned'): void;
}>();
const el = ref<HTMLElement>();
const align = 'left';
function setPosition() {
const rootRect = props.rootElement.getBoundingClientRect();
const rect = props.targetElement.getBoundingClientRect();
const left = props.targetElement.offsetWidth;
const top = (rect.top - rootRect.top) - 8;
el.value.style.left = left + 'px';
el.value.style.top = top + 'px';
}
function onChildClosed(actioned?: boolean) {
if (actioned) {
emit('actioned');
} else {
emit('closed');
}
}
onMounted(() => {
setPosition();
nextTick(() => {
setPosition();
});
});
defineExpose({
checkHit: (ev: MouseEvent) => {
return (ev.target === el.value || el.value.contains(ev.target));
},
});
</script>
<style lang="scss" scoped>
.sfhdhdhr {
position: absolute;
}
</style>

View File

@@ -1,55 +1,67 @@
<template>
<div
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@contextmenu.self="e => e.preventDefault()"
>
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item">
<span>{{ item.text }}</span>
<div>
<div
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@contextmenu.self="e => e.preventDefault()"
>
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item">
<span>{{ item.text }}</span>
</span>
<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
<span><MkEllipsis/></span>
</span>
<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</MkA>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</a>
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
</span>
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<span>{{ item.text }}</span>
<span class="caret"><i class="fas fa-caret-right fa-fw"></i></span>
</button>
<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ $ts.none }}</span>
</span>
<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
<span><MkEllipsis/></span>
</span>
<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close()">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</MkA>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close()">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</a>
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item">
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
</span>
<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ $ts.none }}</span>
</span>
</div>
<div v-if="childMenu" class="child">
<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, watch } from 'vue';
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus';
import FormSwitch from '@/components/form/switch.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
import * as os from '@/os';
const XChild = defineAsyncComponent(() => import('./menu.child.vue'));
const props = defineProps<{
items: MenuItem[];
@@ -61,19 +73,23 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(ev: 'close'): void;
(ev: 'close', actioned?: boolean): void;
}>();
let itemsEl = $ref<HTMLDivElement>();
let items2: InnerMenuItem[] = $ref([]);
let child = $ref<InstanceType<typeof XChild>>();
let keymap = $computed(() => ({
'up|k|shift+tab': focusUp,
'down|j|tab': focusDown,
'esc': close,
}));
let childShowingItem = $ref<MenuItem | null>();
watch(() => props.items, () => {
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
@@ -93,21 +109,53 @@ watch(() => props.items, () => {
immediate: true,
});
onMounted(() => {
if (props.viaKeyboard) {
nextTick(() => {
focusNext(itemsEl.children[0], true, false);
});
let childMenu = $ref<MenuItem[] | null>();
let childTarget = $ref<HTMLElement | null>();
function closeChild() {
childMenu = null;
childShowingItem = null;
}
function childActioned() {
closeChild();
close(true);
}
function onGlobalMousedown(event: MouseEvent) {
if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
if (child && child.checkHit(event)) return;
closeChild();
}
let childCloseTimer: null | number = null;
function onItemMouseEnter(item) {
childCloseTimer = window.setTimeout(() => {
closeChild();
}, 300);
}
function onItemMouseLeave(item) {
if (childCloseTimer) window.clearTimeout(childCloseTimer);
}
async function showChildren(item: MenuItem, ev: MouseEvent) {
if (props.asDrawer) {
os.popupMenu(item.children, ev.currentTarget ?? ev.target);
close();
} else {
childTarget = ev.currentTarget ?? ev.target;
childMenu = item.children;
childShowingItem = item;
}
});
}
function clicked(fn: MenuAction, ev: MouseEvent) {
fn(ev);
close();
close(true);
}
function close() {
emit('close');
function close(actioned = false) {
emit('close', actioned);
}
function focusUp() {
@@ -117,6 +165,20 @@ function focusUp() {
function focusDown() {
focusNext(document.activeElement);
}
onMounted(() => {
if (props.viaKeyboard) {
nextTick(() => {
focusNext(itemsEl.children[0], true, false);
});
}
document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
});
onBeforeUnmount(() => {
document.removeEventListener('mousedown', onGlobalMousedown);
});
</script>
<style lang="scss" scoped>
@@ -225,6 +287,25 @@ function focusDown() {
opacity: 0.7;
}
&.parent {
display: flex;
align-items: center;
cursor: default;
> .caret {
margin-left: auto;
}
&.childShowing {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
}
> i {
margin-right: 5px;
width: 20px;

View File

@@ -1,6 +1,6 @@
<template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
</MkModal>
</template>

View File

@@ -12,6 +12,7 @@
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import * as os from '@/os';
import { calcPopupPosition } from '@/scripts/popup-position';
const props = withDefaults(defineProps<{
showing: boolean;
@@ -36,151 +37,20 @@ const emit = defineEmits<{
const el = ref<HTMLElement>();
const zIndex = os.claimZIndex('high');
const setPosition = () => {
if (el.value == null) return;
function setPosition() {
const data = calcPopupPosition(el.value, {
anchorElement: props.targetElement,
direction: props.direction,
align: 'center',
innerMargin: props.innerMargin,
x: props.x,
y: props.y,
});
const contentWidth = el.value.offsetWidth;
const contentHeight = el.value.offsetHeight;
let rect: DOMRect;
if (props.targetElement) {
rect = props.targetElement.getBoundingClientRect();
}
const calcPosWhenTop = () => {
let left: number;
let top: number;
if (props.targetElement) {
left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
} else {
left = props.x;
top = (props.y - contentHeight) - props.innerMargin;
}
left -= (el.value.offsetWidth / 2);
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
}
return [left, top];
};
const calcPosWhenBottom = () => {
let left: number;
let top: number;
if (props.targetElement) {
left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
top = (rect.top + window.pageYOffset + props.targetElement.offsetHeight) + props.innerMargin;
} else {
left = props.x;
top = (props.y) + props.innerMargin;
}
left -= (el.value.offsetWidth / 2);
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
}
return [left, top];
};
const calcPosWhenLeft = () => {
let left: number;
let top: number;
if (props.targetElement) {
left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
} else {
left = (props.x - contentWidth) - props.innerMargin;
top = props.y;
}
top -= (el.value.offsetHeight / 2);
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
}
return [left, top];
};
const calcPosWhenRight = () => {
let left: number;
let top: number;
if (props.targetElement) {
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;
top = props.y;
}
top -= (el.value.offsetHeight / 2);
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
}
return [left, top];
};
const calc = (): {
left: number;
top: number;
transformOrigin: string;
} => {
switch (props.direction) {
case 'top': {
const [left, top] = calcPosWhenTop();
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
if (top - window.pageYOffset < 0) {
const [left, top] = calcPosWhenBottom();
return { left, top, transformOrigin: 'center top' };
}
return { left, top, transformOrigin: 'center bottom' };
}
case 'bottom': {
const [left, top] = calcPosWhenBottom();
// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
return { left, top, transformOrigin: 'center top' };
}
case 'left': {
const [left, top] = calcPosWhenLeft();
// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
if (left - window.pageXOffset < 0) {
const [left, top] = calcPosWhenRight();
return { left, top, transformOrigin: 'left center' };
}
return { left, top, transformOrigin: 'right center' };
}
case 'right': {
const [left, top] = calcPosWhenRight();
// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
return { left, top, transformOrigin: 'left center' };
}
}
};
const { left, top, transformOrigin } = calc();
el.value.style.transformOrigin = transformOrigin;
el.value.style.left = left + 'px';
el.value.style.top = top + 'px';
};
el.value.style.transformOrigin = data.transformOrigin;
el.value.style.left = data.left + 'px';
el.value.style.top = data.top + 'px';
}
let loopHandler;

View File

@@ -1,6 +1,6 @@
<template>
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
<div v-if="showing" class="ebkgocck">
<div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }">
<div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
<span class="left">
@@ -11,6 +11,8 @@
</span>
<span class="right">
<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
<button v-if="canResize && maximized" class="button _button" @click="unMaximize()"><i class="fas fa-window-restore"></i></button>
<button v-else-if="canResize && !maximized" class="button _button" @click="maximize()"><i class="fas fa-window-maximize"></i></button>
<button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button>
</span>
</div>
@@ -32,15 +34,16 @@
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, provide } from 'vue';
import contains from '@/scripts/contains';
import * as os from '@/os';
import { MenuItem } from '@/types/menu';
const minHeight = 50;
const minWidth = 250;
function dragListen(fn) {
function dragListen(fn: (ev: MouseEvent) => void) {
window.addEventListener('mousemove', fn);
window.addEventListener('touchmove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn));
@@ -56,315 +59,342 @@ function dragClear(fn) {
window.removeEventListener('touchend', dragClear);
}
export default defineComponent({
provide: {
inWindow: true,
},
const props = withDefaults(defineProps<{
initialWidth?: number;
initialHeight?: number | null;
canResize?: boolean;
closeButton?: boolean;
mini?: boolean;
front?: boolean;
contextmenu?: MenuItem[] | null;
buttonsLeft?: any[];
buttonsRight?: any[];
}>(), {
initialWidth: 400,
initialHeight: null,
canResize: false,
closeButton: true,
mini: false,
front: false,
contextmenu: null,
buttonsLeft: () => [],
buttonsRight: () => [],
});
props: {
initialWidth: {
type: Number,
required: false,
default: 400,
},
initialHeight: {
type: Number,
required: false,
default: null,
},
canResize: {
type: Boolean,
required: false,
default: false,
},
closeButton: {
type: Boolean,
required: false,
default: true,
},
mini: {
type: Boolean,
required: false,
default: false,
},
front: {
type: Boolean,
required: false,
default: false,
},
contextmenu: {
type: Array,
required: false,
},
buttonsLeft: {
type: Array,
required: false,
default: () => [],
},
buttonsRight: {
type: Array,
required: false,
default: () => [],
},
},
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
emits: ['closed'],
provide('inWindow', true);
data() {
return {
showing: true,
id: Math.random().toString(), // TODO: UUIDとかにする
};
},
let rootEl = $ref<HTMLElement | null>();
let showing = $ref(true);
let beforeClickedAt = 0;
let maximized = $ref(false);
let unMaximizedTop = '';
let unMaximizedLeft = '';
let unMaximizedWidth = '';
let unMaximizedHeight = '';
mounted() {
if (this.initialWidth) this.applyTransformWidth(this.initialWidth);
if (this.initialHeight) this.applyTransformHeight(this.initialHeight);
function close() {
showing = false;
}
this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2));
this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2));
function onKeydown(evt) {
if (evt.which === 27) { // Esc
evt.preventDefault();
evt.stopPropagation();
close();
}
}
// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
this.top();
function onContextmenu(ev: MouseEvent) {
if (props.contextmenu) {
os.contextMenu(props.contextmenu, ev);
}
}
window.addEventListener('resize', this.onBrowserResize);
},
// 最前面へ移動
function top() {
if (rootEl) {
rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
}
}
unmounted() {
window.removeEventListener('resize', this.onBrowserResize);
},
function maximize() {
maximized = true;
unMaximizedTop = rootEl.style.top;
unMaximizedLeft = rootEl.style.left;
unMaximizedWidth = rootEl.style.width;
unMaximizedHeight = rootEl.style.height;
rootEl.style.top = '0';
rootEl.style.left = '0';
rootEl.style.width = '100%';
rootEl.style.height = '100%';
}
methods: {
close() {
this.showing = false;
},
function unMaximize() {
maximized = false;
rootEl.style.top = unMaximizedTop;
rootEl.style.left = unMaximizedLeft;
rootEl.style.width = unMaximizedWidth;
rootEl.style.height = unMaximizedHeight;
}
onKeydown(evt) {
if (evt.which === 27) { // Esc
evt.preventDefault();
evt.stopPropagation();
this.close();
function onBodyMousedown() {
top();
}
function onDblClick() {
maximize();
}
function onHeaderMousedown(evt: MouseEvent) {
// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
if (evt.button === 2) return;
let beforeMaximized = false;
if (maximized) {
beforeMaximized = true;
unMaximize();
}
// ダブルクリック判定
if (Date.now() - beforeClickedAt < 300) {
beforeClickedAt = Date.now();
onDblClick();
return;
}
beforeClickedAt = Date.now();
const main = rootEl;
if (!contains(main, document.activeElement)) main.focus();
const position = main.getBoundingClientRect();
const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
const moveBaseX = beforeMaximized ? parseInt(unMaximizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
const browserWidth = window.innerWidth;
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
const windowHeight = main.offsetHeight;
function move(x: number, y: number) {
let moveLeft = x - moveBaseX;
let moveTop = y - moveBaseY;
// 下はみ出し
if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
// 左はみ出し
if (moveLeft < 0) moveLeft = 0;
// 上はみ出し
if (moveTop < 0) moveTop = 0;
// 右はみ出し
if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
rootEl.style.left = moveLeft + 'px';
rootEl.style.top = moveTop + 'px';
}
if (beforeMaximized) {
move(clickX, clickY);
}
// 動かした時
dragListen(me => {
const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
move(x, y);
});
}
// 上ハンドル掴み時
function onTopHandleMousedown(evt) {
const main = rootEl;
const base = evt.clientY;
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
// 動かした時
dragListen(me => {
const move = me.clientY - base;
if (top + move > 0) {
if (height + -move > minHeight) {
applyTransformHeight(height + -move);
applyTransformTop(top + move);
} else { // 最小の高さより小さくなろうとした時
applyTransformHeight(minHeight);
applyTransformTop(top + (height - minHeight));
}
},
} else { // 上のはみ出し時
applyTransformHeight(top + height);
applyTransformTop(0);
}
});
}
onContextmenu(ev: MouseEvent) {
if (this.contextmenu) {
os.contextMenu(this.contextmenu, ev);
// 右ハンドル掴み時
function onRightHandleMousedown(evt) {
const main = rootEl;
const base = evt.clientX;
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const browserWidth = window.innerWidth;
// 動かした時
dragListen(me => {
const move = me.clientX - base;
if (left + width + move < browserWidth) {
if (width + move > minWidth) {
applyTransformWidth(width + move);
} else { // 最小の幅より小さくなろうとした時
applyTransformWidth(minWidth);
}
},
} else { // 右のはみ出し時
applyTransformWidth(browserWidth - left);
}
});
}
// 最前面へ移動
top() {
(this.$el as any).style.zIndex = os.claimZIndex(this.front ? 'middle' : 'low');
},
// 下ハンドル掴み時
function onBottomHandleMousedown(evt) {
const main = rootEl;
onBodyMousedown() {
this.top();
},
const base = evt.clientY;
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const browserHeight = window.innerHeight;
onHeaderMousedown(evt: MouseEvent) {
// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
if (evt.button === 2) return;
// 動かした時
dragListen(me => {
const move = me.clientY - base;
if (top + height + move < browserHeight) {
if (height + move > minHeight) {
applyTransformHeight(height + move);
} else { // 最小の高さより小さくなろうとした時
applyTransformHeight(minHeight);
}
} else { // 下のはみ出し時
applyTransformHeight(browserHeight - top);
}
});
}
const main = this.$el as any;
// 左ハンドル掴み時
function onLeftHandleMousedown(evt) {
const main = rootEl;
if (!contains(main, document.activeElement)) main.focus();
const base = evt.clientX;
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const position = main.getBoundingClientRect();
// 動かした時
dragListen(me => {
const move = me.clientX - base;
if (left + move > 0) {
if (width + -move > minWidth) {
applyTransformWidth(width + -move);
applyTransformLeft(left + move);
} else { // 最小の幅より小さくなろうとした時
applyTransformWidth(minWidth);
applyTransformLeft(left + (width - minWidth));
}
} else { // 左のはみ出し時
applyTransformWidth(left + width);
applyTransformLeft(0);
}
});
}
const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
const moveBaseX = clickX - position.left;
const moveBaseY = clickY - position.top;
const browserWidth = window.innerWidth;
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
const windowHeight = main.offsetHeight;
// 左上ハンドル掴み時
function onTopLeftHandleMousedown(evt) {
onTopHandleMousedown(evt);
onLeftHandleMousedown(evt);
}
// 動かした
dragListen(me => {
const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
// 右上ハンドル掴み
function onTopRightHandleMousedown(evt) {
onTopHandleMousedown(evt);
onRightHandleMousedown(evt);
}
let moveLeft = x - moveBaseX;
let moveTop = y - moveBaseY;
// 右下ハンドル掴み時
function onBottomRightHandleMousedown(evt) {
onBottomHandleMousedown(evt);
onRightHandleMousedown(evt);
}
// 下はみ出し
if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
// 左下ハンドル掴み時
function onBottomLeftHandleMousedown(evt) {
onBottomHandleMousedown(evt);
onLeftHandleMousedown(evt);
}
// 左はみ出し
if (moveLeft < 0) moveLeft = 0;
// 高さを適用
function applyTransformHeight(height) {
if (height > window.innerHeight) height = window.innerHeight;
rootEl.style.height = height + 'px';
}
// 上はみ出し
if (moveTop < 0) moveTop = 0;
// 幅を適用
function applyTransformWidth(width) {
if (width > window.innerWidth) width = window.innerWidth;
rootEl.style.width = width + 'px';
}
// 右はみ出し
if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
// Y座標を適用
function applyTransformTop(top) {
rootEl.style.top = top + 'px';
}
this.$el.style.left = moveLeft + 'px';
this.$el.style.top = moveTop + 'px';
});
},
// X座標を適用
function applyTransformLeft(left) {
rootEl.style.left = left + 'px';
}
// 上ハンドル掴み時
onTopHandleMousedown(evt) {
const main = this.$el as any;
function onBrowserResize() {
const main = rootEl;
const position = main.getBoundingClientRect();
const browserWidth = window.innerWidth;
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
const windowHeight = main.offsetHeight;
if (position.left < 0) main.style.left = '0'; // 左はみ出し
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
if (position.top < 0) main.style.top = '0'; // 上はみ出し
}
const base = evt.clientY;
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
onMounted(() => {
if (props.initialWidth) applyTransformWidth(props.initialWidth);
if (props.initialHeight) applyTransformHeight(props.initialHeight);
// 動かした時
dragListen(me => {
const move = me.clientY - base;
if (top + move > 0) {
if (height + -move > minHeight) {
this.applyTransformHeight(height + -move);
this.applyTransformTop(top + move);
} else { // 最小の高さより小さくなろうとした時
this.applyTransformHeight(minHeight);
this.applyTransformTop(top + (height - minHeight));
}
} else { // 上のはみ出し時
this.applyTransformHeight(top + height);
this.applyTransformTop(0);
}
});
},
applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2));
applyTransformLeft((window.innerWidth / 2) - (rootEl.offsetWidth / 2));
// 右ハンドル掴み時
onRightHandleMousedown(evt) {
const main = this.$el as any;
// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
top();
const base = evt.clientX;
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const browserWidth = window.innerWidth;
window.addEventListener('resize', onBrowserResize);
});
// 動かした時
dragListen(me => {
const move = me.clientX - base;
if (left + width + move < browserWidth) {
if (width + move > minWidth) {
this.applyTransformWidth(width + move);
} else { // 最小の幅より小さくなろうとした時
this.applyTransformWidth(minWidth);
}
} else { // 右のはみ出し時
this.applyTransformWidth(browserWidth - left);
}
});
},
onBeforeUnmount(() => {
window.removeEventListener('resize', onBrowserResize);
});
// 下ハンドル掴み時
onBottomHandleMousedown(evt) {
const main = this.$el as any;
const base = evt.clientY;
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const browserHeight = window.innerHeight;
// 動かした時
dragListen(me => {
const move = me.clientY - base;
if (top + height + move < browserHeight) {
if (height + move > minHeight) {
this.applyTransformHeight(height + move);
} else { // 最小の高さより小さくなろうとした時
this.applyTransformHeight(minHeight);
}
} else { // 下のはみ出し時
this.applyTransformHeight(browserHeight - top);
}
});
},
// 左ハンドル掴み時
onLeftHandleMousedown(evt) {
const main = this.$el as any;
const base = evt.clientX;
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
// 動かした時
dragListen(me => {
const move = me.clientX - base;
if (left + move > 0) {
if (width + -move > minWidth) {
this.applyTransformWidth(width + -move);
this.applyTransformLeft(left + move);
} else { // 最小の幅より小さくなろうとした時
this.applyTransformWidth(minWidth);
this.applyTransformLeft(left + (width - minWidth));
}
} else { // 左のはみ出し時
this.applyTransformWidth(left + width);
this.applyTransformLeft(0);
}
});
},
// 左上ハンドル掴み時
onTopLeftHandleMousedown(evt) {
this.onTopHandleMousedown(evt);
this.onLeftHandleMousedown(evt);
},
// 右上ハンドル掴み時
onTopRightHandleMousedown(evt) {
this.onTopHandleMousedown(evt);
this.onRightHandleMousedown(evt);
},
// 右下ハンドル掴み時
onBottomRightHandleMousedown(evt) {
this.onBottomHandleMousedown(evt);
this.onRightHandleMousedown(evt);
},
// 左下ハンドル掴み時
onBottomLeftHandleMousedown(evt) {
this.onBottomHandleMousedown(evt);
this.onLeftHandleMousedown(evt);
},
// 高さを適用
applyTransformHeight(height) {
if (height > window.innerHeight) height = window.innerHeight;
(this.$el as any).style.height = height + 'px';
},
// 幅を適用
applyTransformWidth(width) {
if (width > window.innerWidth) width = window.innerWidth;
(this.$el as any).style.width = width + 'px';
},
// Y座標を適用
applyTransformTop(top) {
(this.$el as any).style.top = top + 'px';
},
// X座標を適用
applyTransformLeft(left) {
(this.$el as any).style.left = left + 'px';
},
onBrowserResize() {
const main = this.$el as any;
const position = main.getBoundingClientRect();
const browserWidth = window.innerWidth;
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
const windowHeight = main.offsetHeight;
if (position.left < 0) main.style.left = 0; // 左はみ出し
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
if (position.top < 0) main.style.top = 0; // 上はみ出し
},
},
defineExpose({
close,
});
</script>
@@ -384,7 +414,7 @@ export default defineComponent({
left: 0;
> .body {
overflow: hidden;
overflow: clip;
display: flex;
flex-direction: column;
contain: content;
@@ -522,5 +552,11 @@ export default defineComponent({
cursor: nesw-resize;
}
}
&.maximized {
> .body {
border-radius: 0;
}
}
}
</style>

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

@@ -71,7 +71,7 @@ export class Router extends EventEmitter<{
private currentKey = Date.now().toString();
public currentRoute: ShallowRef<RouteDef | null> = shallowRef(null);
public navHook: ((path: string) => boolean) | null = null;
public navHook: ((path: string, flag?: any) => boolean) | null = null;
constructor(routes: Router['routes'], currentPath: Router['currentPath']) {
super();
@@ -213,14 +213,14 @@ export class Router extends EventEmitter<{
return this.currentKey;
}
public push(path: string) {
public push(path: string, flag?: any) {
const beforePath = this.currentPath;
if (path === beforePath) {
this.emit('same');
return;
}
if (this.navHook) {
const cancel = this.navHook(path);
const cancel = this.navHook(path, flag);
if (cancel) return;
}
this.navigate(path, null);

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

@@ -1,301 +1,313 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<div class="mwysmxbg">
<div>{{ $ts._mfm.intro }}</div>
<MkSpacer :content-max="800">
<div class="mwysmxbg">
<div>{{ $ts._mfm.intro }}</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.mention }}</div>
<div class="content">
<p>{{ $ts._mfm.mentionDescription }}</p>
<div class="preview">
<Mfm :text="preview_mention"/>
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.hashtag }}</div>
<div class="content">
<p>{{ $ts._mfm.hashtagDescription }}</p>
<div class="preview">
<Mfm :text="preview_hashtag"/>
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.url }}</div>
<div class="content">
<p>{{ $ts._mfm.urlDescription }}</p>
<div class="preview">
<Mfm :text="preview_url"/>
<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.link }}</div>
<div class="content">
<p>{{ $ts._mfm.linkDescription }}</p>
<div class="preview">
<Mfm :text="preview_link"/>
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.emoji }}</div>
<div class="content">
<p>{{ $ts._mfm.emojiDescription }}</p>
<div class="preview">
<Mfm :text="preview_emoji"/>
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.bold }}</div>
<div class="content">
<p>{{ $ts._mfm.boldDescription }}</p>
<div class="preview">
<Mfm :text="preview_bold"/>
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.small }}</div>
<div class="content">
<p>{{ $ts._mfm.smallDescription }}</p>
<div class="preview">
<Mfm :text="preview_small"/>
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.quote }}</div>
<div class="content">
<p>{{ $ts._mfm.quoteDescription }}</p>
<div class="preview">
<Mfm :text="preview_quote"/>
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.center }}</div>
<div class="content">
<p>{{ $ts._mfm.centerDescription }}</p>
<div class="preview">
<Mfm :text="preview_center"/>
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.inlineCode }}</div>
<div class="content">
<p>{{ $ts._mfm.inlineCodeDescription }}</p>
<div class="preview">
<Mfm :text="preview_inlineCode"/>
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.blockCode }}</div>
<div class="content">
<p>{{ $ts._mfm.blockCodeDescription }}</p>
<div class="preview">
<Mfm :text="preview_blockCode"/>
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.inlineMath }}</div>
<div class="content">
<p>{{ $ts._mfm.inlineMathDescription }}</p>
<div class="preview">
<Mfm :text="preview_inlineMath"/>
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<!-- deprecated
<div class="section _block">
<div class="title">{{ $ts._mfm.mention }}</div>
<div class="title">{{ $ts._mfm.search }}</div>
<div class="content">
<p>{{ $ts._mfm.mentionDescription }}</p>
<p>{{ $ts._mfm.searchDescription }}</p>
<div class="preview">
<Mfm :text="preview_mention"/>
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
<Mfm :text="preview_search"/>
<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.hashtag }}</div>
<div class="content">
<p>{{ $ts._mfm.hashtagDescription }}</p>
<div class="preview">
<Mfm :text="preview_hashtag"/>
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
-->
<div class="section _block">
<div class="title">{{ $ts._mfm.flip }}</div>
<div class="content">
<p>{{ $ts._mfm.flipDescription }}</p>
<div class="preview">
<Mfm :text="preview_flip"/>
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.font }}</div>
<div class="content">
<p>{{ $ts._mfm.fontDescription }}</p>
<div class="preview">
<Mfm :text="preview_font"/>
<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.x2 }}</div>
<div class="content">
<p>{{ $ts._mfm.x2Description }}</p>
<div class="preview">
<Mfm :text="preview_x2"/>
<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.x3 }}</div>
<div class="content">
<p>{{ $ts._mfm.x3Description }}</p>
<div class="preview">
<Mfm :text="preview_x3"/>
<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.x4 }}</div>
<div class="content">
<p>{{ $ts._mfm.x4Description }}</p>
<div class="preview">
<Mfm :text="preview_x4"/>
<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.blur }}</div>
<div class="content">
<p>{{ $ts._mfm.blurDescription }}</p>
<div class="preview">
<Mfm :text="preview_blur"/>
<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.jelly }}</div>
<div class="content">
<p>{{ $ts._mfm.jellyDescription }}</p>
<div class="preview">
<Mfm :text="preview_jelly"/>
<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.tada }}</div>
<div class="content">
<p>{{ $ts._mfm.tadaDescription }}</p>
<div class="preview">
<Mfm :text="preview_tada"/>
<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.jump }}</div>
<div class="content">
<p>{{ $ts._mfm.jumpDescription }}</p>
<div class="preview">
<Mfm :text="preview_jump"/>
<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.bounce }}</div>
<div class="content">
<p>{{ $ts._mfm.bounceDescription }}</p>
<div class="preview">
<Mfm :text="preview_bounce"/>
<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.spin }}</div>
<div class="content">
<p>{{ $ts._mfm.spinDescription }}</p>
<div class="preview">
<Mfm :text="preview_spin"/>
<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.shake }}</div>
<div class="content">
<p>{{ $ts._mfm.shakeDescription }}</p>
<div class="preview">
<Mfm :text="preview_shake"/>
<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.twitch }}</div>
<div class="content">
<p>{{ $ts._mfm.twitchDescription }}</p>
<div class="preview">
<Mfm :text="preview_twitch"/>
<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.rainbow }}</div>
<div class="content">
<p>{{ $ts._mfm.rainbowDescription }}</p>
<div class="preview">
<Mfm :text="preview_rainbow"/>
<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.sparkle }}</div>
<div class="content">
<p>{{ $ts._mfm.sparkleDescription }}</p>
<div class="preview">
<Mfm :text="preview_sparkle"/>
<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.rotate }}</div>
<div class="content">
<p>{{ $ts._mfm.rotateDescription }}</p>
<div class="preview">
<Mfm :text="preview_rotate"/>
<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.plain }}</div>
<div class="content">
<p>{{ $ts._mfm.plainDescription }}</p>
<div class="preview">
<Mfm :text="preview_plain"/>
<MkTextarea v-model="preview_plain"><span>MFM</span></MkTextarea>
</div>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.url }}</div>
<div class="content">
<p>{{ $ts._mfm.urlDescription }}</p>
<div class="preview">
<Mfm :text="preview_url"/>
<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.link }}</div>
<div class="content">
<p>{{ $ts._mfm.linkDescription }}</p>
<div class="preview">
<Mfm :text="preview_link"/>
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.emoji }}</div>
<div class="content">
<p>{{ $ts._mfm.emojiDescription }}</p>
<div class="preview">
<Mfm :text="preview_emoji"/>
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.bold }}</div>
<div class="content">
<p>{{ $ts._mfm.boldDescription }}</p>
<div class="preview">
<Mfm :text="preview_bold"/>
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.small }}</div>
<div class="content">
<p>{{ $ts._mfm.smallDescription }}</p>
<div class="preview">
<Mfm :text="preview_small"/>
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.quote }}</div>
<div class="content">
<p>{{ $ts._mfm.quoteDescription }}</p>
<div class="preview">
<Mfm :text="preview_quote"/>
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.center }}</div>
<div class="content">
<p>{{ $ts._mfm.centerDescription }}</p>
<div class="preview">
<Mfm :text="preview_center"/>
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.inlineCode }}</div>
<div class="content">
<p>{{ $ts._mfm.inlineCodeDescription }}</p>
<div class="preview">
<Mfm :text="preview_inlineCode"/>
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.blockCode }}</div>
<div class="content">
<p>{{ $ts._mfm.blockCodeDescription }}</p>
<div class="preview">
<Mfm :text="preview_blockCode"/>
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.inlineMath }}</div>
<div class="content">
<p>{{ $ts._mfm.inlineMathDescription }}</p>
<div class="preview">
<Mfm :text="preview_inlineMath"/>
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<!-- deprecated
<div class="section _block">
<div class="title">{{ $ts._mfm.search }}</div>
<div class="content">
<p>{{ $ts._mfm.searchDescription }}</p>
<div class="preview">
<Mfm :text="preview_search"/>
<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
-->
<div class="section _block">
<div class="title">{{ $ts._mfm.flip }}</div>
<div class="content">
<p>{{ $ts._mfm.flipDescription }}</p>
<div class="preview">
<Mfm :text="preview_flip"/>
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.font }}</div>
<div class="content">
<p>{{ $ts._mfm.fontDescription }}</p>
<div class="preview">
<Mfm :text="preview_font"/>
<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.x2 }}</div>
<div class="content">
<p>{{ $ts._mfm.x2Description }}</p>
<div class="preview">
<Mfm :text="preview_x2"/>
<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.x3 }}</div>
<div class="content">
<p>{{ $ts._mfm.x3Description }}</p>
<div class="preview">
<Mfm :text="preview_x3"/>
<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.x4 }}</div>
<div class="content">
<p>{{ $ts._mfm.x4Description }}</p>
<div class="preview">
<Mfm :text="preview_x4"/>
<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.blur }}</div>
<div class="content">
<p>{{ $ts._mfm.blurDescription }}</p>
<div class="preview">
<Mfm :text="preview_blur"/>
<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.jelly }}</div>
<div class="content">
<p>{{ $ts._mfm.jellyDescription }}</p>
<div class="preview">
<Mfm :text="preview_jelly"/>
<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.tada }}</div>
<div class="content">
<p>{{ $ts._mfm.tadaDescription }}</p>
<div class="preview">
<Mfm :text="preview_tada"/>
<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.jump }}</div>
<div class="content">
<p>{{ $ts._mfm.jumpDescription }}</p>
<div class="preview">
<Mfm :text="preview_jump"/>
<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.bounce }}</div>
<div class="content">
<p>{{ $ts._mfm.bounceDescription }}</p>
<div class="preview">
<Mfm :text="preview_bounce"/>
<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.spin }}</div>
<div class="content">
<p>{{ $ts._mfm.spinDescription }}</p>
<div class="preview">
<Mfm :text="preview_spin"/>
<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.shake }}</div>
<div class="content">
<p>{{ $ts._mfm.shakeDescription }}</p>
<div class="preview">
<Mfm :text="preview_shake"/>
<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.twitch }}</div>
<div class="content">
<p>{{ $ts._mfm.twitchDescription }}</p>
<div class="preview">
<Mfm :text="preview_twitch"/>
<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.rainbow }}</div>
<div class="content">
<p>{{ $ts._mfm.rainbowDescription }}</p>
<div class="preview">
<Mfm :text="preview_rainbow"/>
<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.sparkle }}</div>
<div class="content">
<p>{{ $ts._mfm.sparkleDescription }}</p>
<div class="preview">
<Mfm :text="preview_sparkle"/>
<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.rotate }}</div>
<div class="content">
<p>{{ $ts._mfm.rotateDescription }}</p>
<div class="preview">
<Mfm :text="preview_rotate"/>
<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
</div>
</div>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -306,35 +318,36 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
const preview_mention = '@example';
const preview_hashtag = '#test';
const preview_url = 'https://example.com';
const preview_link = `[${i18n.ts._mfm.dummy}](https://example.com)`;
const preview_emoji = instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:';
const preview_bold = `**${i18n.ts._mfm.dummy}**`;
const preview_small = `<small>${i18n.ts._mfm.dummy}</small>`;
const preview_center = `<center>${i18n.ts._mfm.dummy}</center>`;
const preview_inlineCode = '`<: "Hello, world!"`';
const preview_blockCode = '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```';
const preview_inlineMath = '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)';
const preview_quote = `> ${i18n.ts._mfm.dummy}`;
const preview_search = `${i18n.ts._mfm.dummy} 検索`;
const preview_jelly = '$[jelly 🍮] $[jelly.speed=5s 🍮]';
const preview_tada = '$[tada 🍮] $[tada.speed=5s 🍮]';
const preview_jump = '$[jump 🍮] $[jump.speed=5s 🍮]';
const preview_bounce = '$[bounce 🍮] $[bounce.speed=5s 🍮]';
const preview_shake = '$[shake 🍮] $[shake.speed=5s 🍮]';
const preview_twitch = '$[twitch 🍮] $[twitch.speed=5s 🍮]';
const preview_spin = '$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]';
const preview_flip = `$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`;
const preview_font = `$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`;
const preview_x2 = '$[x2 🍮]';
const preview_x3 = '$[x3 🍮]';
const preview_x4 = '$[x4 🍮]';
const preview_blur = `$[blur ${i18n.ts._mfm.dummy}]`;
const preview_rainbow = '$[rainbow 🍮] $[rainbow.speed=5s 🍮]';
const preview_sparkle = '$[sparkle 🍮]';
const preview_rotate = '$[rotate 🍮]';
let preview_mention = $ref('@example');
let preview_hashtag = $ref('#test');
let preview_url = $ref('https://example.com');
let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`);
let preview_emoji = $ref(instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:');
let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`);
let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
let preview_inlineCode = $ref('`<: "Hello, world!"`');
let preview_blockCode = $ref('```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```');
let preview_inlineMath = $ref('\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)');
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
let preview_jelly = $ref('$[jelly 🍮] $[jelly.speed=5s 🍮]');
let preview_tada = $ref('$[tada 🍮] $[tada.speed=5s 🍮]');
let preview_jump = $ref('$[jump 🍮] $[jump.speed=5s 🍮]');
let preview_bounce = $ref('$[bounce 🍮] $[bounce.speed=5s 🍮]');
let preview_shake = $ref('$[shake 🍮] $[shake.speed=5s 🍮]');
let preview_twitch = $ref('$[twitch 🍮] $[twitch.speed=5s 🍮]');
let preview_spin = $ref('$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]');
let preview_flip = $ref(`$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`);
let preview_font = $ref(`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`);
let preview_x2 = $ref('$[x2 🍮]');
let preview_x3 = $ref('$[x3 🍮]');
let preview_x4 = $ref('$[x4 🍮]');
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
let preview_rainbow = $ref('$[rainbow 🍮] $[rainbow.speed=5s 🍮]');
let preview_sparkle = $ref('$[sparkle 🍮]');
let preview_rotate = $ref('$[rotate 🍮]');
let preview_plain = $ref('<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>');
definePageMetadata({
title: i18n.ts._mfm.cheatSheet,

View File

@@ -0,0 +1,96 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="600" :margin-min="16">
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.domain }}</template>
<template #value>{{ $ts.system }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.scope }}</template>
<template #value>{{ scope.join('/') }}</template>
</MkKeyValue>
</FormSplit>
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<FormSection v-if="keys">
<template #label>{{ i18n.ts.keys }}</template>
<div class="_formLinks">
<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
</div>
</FormSection>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
import MkKeyValue from '@/components/key-value.vue';
import FormSplit from '@/components/form/split.vue';
const props = defineProps<{
path: string;
}>();
const scope = $computed(() => props.path.split('/'));
let keys = $ref(null);
function fetchKeys() {
os.api('i/registry/keys-with-type', {
scope: scope,
}).then(res => {
keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
});
}
async function createKey() {
const { canceled, result } = await os.form(i18n.ts._registry.createKey, {
key: {
type: 'string',
label: i18n.ts._registry.key,
},
value: {
type: 'string',
multiline: true,
label: i18n.ts.value,
},
scope: {
type: 'string',
label: i18n.ts._registry.scope,
default: scope.join('/'),
},
});
if (canceled) return;
os.apiWithDialog('i/registry/set', {
scope: result.scope.split('/'),
key: result.key,
value: JSON5.parse(result.value),
}).then(() => {
fetchKeys();
});
}
watch(() => props.path, fetchKeys, { immediate: true });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
icon: 'fas fa-cogs',
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,123 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="600" :margin-min="16">
<FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo>
<template v-if="value">
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.domain }}</template>
<template #value>{{ $ts.system }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.scope }}</template>
<template #value>{{ scope.join('/') }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.key }}</template>
<template #value>{{ key }}</template>
</MkKeyValue>
</FormSplit>
<FormTextarea v-model="valueForEditor" tall class="_formBlock _monospace">
<template #label>{{ $ts.value }} (JSON)</template>
</FormTextarea>
<MkButton class="_formBlock" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.updatedAt }}</template>
<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
</MkKeyValue>
<MkButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</MkButton>
</template>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
import MkKeyValue from '@/components/key-value.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSplit from '@/components/form/split.vue';
import FormInfo from '@/components/ui/info.vue';
const props = defineProps<{
path: string;
}>();
const scope = $computed(() => props.path.split('/').slice(0, -1));
const key = $computed(() => props.path.split('/').at(-1));
let value = $ref(null);
let valueForEditor = $ref(null);
function fetchValue() {
os.api('i/registry/get-detail', {
scope,
key,
}).then(res => {
value = res;
valueForEditor = JSON5.stringify(res.value, null, '\t');
});
}
async function save() {
try {
JSON5.parse(valueForEditor);
} catch (err) {
os.alert({
type: 'error',
text: i18n.ts.invalidValue,
});
return;
}
os.confirm({
type: 'warning',
text: i18n.ts.saveConfirm,
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('i/registry/set', {
scope,
key,
value: JSON5.parse(valueForEditor),
});
});
}
function del() {
os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('i/registry/remove', {
scope,
key,
});
});
}
watch(() => props.path, fetchValue, { immediate: true });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
icon: 'fas fa-cogs',
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,74 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="600" :margin-min="16">
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<FormSection v-if="scopes">
<template #label>{{ i18n.ts.system }}</template>
<div class="_formLinks">
<FormLink v-for="scope in scopes" :to="`/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
</div>
</FormSection>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
let scopes = $ref(null);
function fetchScopes() {
os.api('i/registry/scopes').then(res => {
scopes = res.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
});
}
async function createKey() {
const { canceled, result } = await os.form(i18n.ts._registry.createKey, {
key: {
type: 'string',
label: i18n.ts._registry.key,
},
value: {
type: 'string',
multiline: true,
label: i18n.ts.value,
},
scope: {
type: 'string',
label: i18n.ts._registry.scope,
},
});
if (canceled) return;
os.apiWithDialog('i/registry/set', {
scope: result.scope.split('/'),
key: result.key,
value: JSON5.parse(result.value),
}).then(() => {
fetchScopes();
});
}
fetchScopes();
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
icon: 'fas fa-cogs',
});
</script>
<style lang="scss" scoped>
</style>

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

@@ -10,7 +10,7 @@
</FormSection>
<FormSection>
<FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
<FormSwitch :model-value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
{{ $ts.receiveAnnouncementFromInstance }}
</FormSwitch>
</FormSection>

View File

@@ -330,13 +330,11 @@ definePageMetadata(INFO);
width: 34%;
padding-right: 32px;
box-sizing: border-box;
overflow: auto;
}
> .main {
flex: 1;
min-width: 0;
overflow: auto;
}
}
}

View File

@@ -10,6 +10,8 @@
<FormLink to="/settings/account-info" class="_formBlock">{{ i18n.ts.accountInfo }}</FormLink>
<FormLink to="/registry" class="_formBlock"><template #icon><i class="fas fa-cogs"></i></template>{{ i18n.ts.registry }}</FormLink>
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink>
</div>
</template>

View File

@@ -1,11 +1,11 @@
<template>
<div class="_formRoot">
<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div class="avatar _acrylic">
<div class="avatar">
<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
<MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
<MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
</div>
<MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
<MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
</div>
<FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
@@ -39,10 +39,10 @@
<div class="_formRoot">
<FormSplit v-for="(record, i) in fields" :min-width="250" class="_formBlock">
<FormInput v-model="record.name">
<FormInput v-model="record.name" small>
<template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template>
</FormInput>
<FormInput v-model="record.value">
<FormInput v-model="record.value" small>
<template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template>
</FormInput>
</FormSplit>
@@ -187,6 +187,7 @@ definePageMetadata({
position: relative;
background-size: cover;
background-position: center;
border: solid 1px var(--divider);
border-radius: 10px;
overflow: clip;

View File

@@ -6,7 +6,7 @@
<div v-if="user">
<XFollowList :user="user" type="followers"/>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
<MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/>
</transition>
</MkSpacer>

View File

@@ -6,7 +6,7 @@
<div v-if="user">
<XFollowList :user="user" type="following"/>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
<MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/>
</transition>
</MkSpacer>

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

@@ -10,7 +10,7 @@
<XPages v-else-if="tab === 'pages'" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
<MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/>
</transition>
</div>
@@ -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

@@ -153,6 +153,15 @@ export const routes = [{
}, {
path: '/channels',
component: page(() => import('./pages/channels.vue')),
}, {
path: '/registry/keys/system/:path(*)?',
component: page(() => import('./pages/registry.keys.vue')),
}, {
path: '/registry/value/system/:path(*)?',
component: page(() => import('./pages/registry.value.vue')),
}, {
path: '/registry',
component: page(() => import('./pages/registry.vue')),
}, {
path: '/admin/file/:fileId',
component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')),

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,158 @@
import { Ref } from 'vue';
export function calcPopupPosition(el: HTMLElement, props: {
anchorElement: HTMLElement | null;
innerMargin: number;
direction: 'top' | 'bottom' | 'left' | 'right';
align: 'top' | 'bottom' | 'left' | 'right' | 'center';
alignOffset?: number;
x?: number;
y?: number;
}): { top: number; left: number; transformOrigin: string; } {
const contentWidth = el.offsetWidth;
const contentHeight = el.offsetHeight;
let rect: DOMRect;
if (props.anchorElement) {
rect = props.anchorElement.getBoundingClientRect();
}
const calcPosWhenTop = () => {
let left: number;
let top: number;
if (props.anchorElement) {
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
} else {
left = props.x;
top = (props.y - contentHeight) - props.innerMargin;
}
left -= (el.offsetWidth / 2);
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
}
return [left, top];
};
const calcPosWhenBottom = () => {
let left: number;
let top: number;
if (props.anchorElement) {
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin;
} else {
left = props.x;
top = (props.y) + props.innerMargin;
}
left -= (el.offsetWidth / 2);
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
}
return [left, top];
};
const calcPosWhenLeft = () => {
let left: number;
let top: number;
if (props.anchorElement) {
left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
} else {
left = (props.x - contentWidth) - props.innerMargin;
top = props.y;
}
top -= (el.offsetHeight / 2);
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
}
return [left, top];
};
const calcPosWhenRight = () => {
let left: number;
let top: number;
if (props.anchorElement) {
left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin;
if (props.align === 'top') {
top = rect.top + window.pageYOffset;
if (props.alignOffset != null) top += props.alignOffset;
} else if (props.align === 'bottom') {
// TODO
} else { // center
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
top -= (el.offsetHeight / 2);
}
} else {
left = props.x + props.innerMargin;
top = props.y;
top -= (el.offsetHeight / 2);
}
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
}
return [left, top];
};
const calc = (): {
left: number;
top: number;
transformOrigin: string;
} => {
switch (props.direction) {
case 'top': {
const [left, top] = calcPosWhenTop();
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
if (top - window.pageYOffset < 0) {
const [left, top] = calcPosWhenBottom();
return { left, top, transformOrigin: 'center top' };
}
return { left, top, transformOrigin: 'center bottom' };
}
case 'bottom': {
const [left, top] = calcPosWhenBottom();
// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
return { left, top, transformOrigin: 'center top' };
}
case 'left': {
const [left, top] = calcPosWhenLeft();
// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
if (left - window.pageXOffset < 0) {
const [left, top] = calcPosWhenRight();
return { left, top, transformOrigin: 'left center' };
}
return { left, top, transformOrigin: 'right center' };
}
case 'right': {
const [left, top] = calcPosWhenRight();
// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
return { left, top, transformOrigin: 'left center' };
}
}
};
return calc();
}

View File

@@ -270,7 +270,7 @@ type Plugin = {
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
*/
import lightTheme from '@/themes/l-light.json5';
import darkTheme from '@/themes/d-dark.json5';
import darkTheme from '@/themes/d-green-lime.json5';
export class ColdDeviceStorage {
public static default = {

View File

@@ -11,10 +11,11 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] };
export type MenuPending = { type: 'pending' };
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>;
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent>;
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;

View File

@@ -1,5 +1,6 @@
<template>
<component :is="popup.component"
<component
:is="popup.component"
v-for="popup in popups"
:key="popup.id"
v-bind="popup.props"
@@ -15,56 +16,45 @@
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { swInject } from './sw-inject';
import { popup, popups, pendingApiRequestsCount } from '@/os';
import { uploads } from '@/scripts/upload';
import * as sound from '@/scripts/sound';
import { $i } from '@/account';
import { swInject } from './sw-inject';
import { stream } from '@/stream';
export default defineComponent({
components: {
XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
XUpload: defineAsyncComponent(() => import('./upload.vue')),
},
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
setup() {
const onNotification = notification => {
if ($i.mutingNotificationTypes.includes(notification.type)) return;
const dev = _DEV_;
if (document.visibilityState === 'visible') {
stream.send('readNotification', {
id: notification.id
});
const onNotification = notification => {
if ($i.mutingNotificationTypes.includes(notification.type)) return;
popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
notification
}, {}, 'closed');
}
if (document.visibilityState === 'visible') {
stream.send('readNotification', {
id: notification.id,
});
sound.play('notification');
};
popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
notification,
}, {}, 'closed');
}
if ($i) {
const connection = stream.useChannel('main', null, 'UI');
connection.on('notification', onNotification);
sound.play('notification');
};
//#region Listen message from SW
if ('serviceWorker' in navigator) {
swInject();
}
}
if ($i) {
const connection = stream.useChannel('main', null, 'UI');
connection.on('notification', onNotification);
return {
uploads,
popups,
pendingApiRequestsCount,
dev: _DEV_,
};
},
});
//#region Listen message from SW
if ('serviceWorker' in navigator) {
swInject();
}
}
</script>
<style lang="scss">

View File

@@ -3,7 +3,7 @@
<div class="body">
<div class="top">
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
<button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
<button v-click-anime class="item _button instance" @click="openInstanceMenu">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
</button>
</div>
@@ -87,6 +87,36 @@ function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.federation,
icon: 'fas fa-globe',
to: '/about#federation',
}, null, {
type: 'parent',
text: i18n.ts.help,
icon: 'fas fa-question-circle',
children: [{
type: 'link',
to: '/mfm-cheat-sheet',
text: i18n.ts._mfm.cheatSheet,
icon: 'fas fa-code',
}, {
type: 'link',
to: '/scratchpad',
text: i18n.ts.scratchpad,
icon: 'fas fa-terminal',
}, {
type: 'link',
to: '/api-console',
text: 'API Console',
icon: 'fas fa-terminal',
}, null, {
text: i18n.ts.document,
icon: 'fas fa-question-circle',
action: () => {
window.open('https://misskey-hub.net/help.html', '_blank');
},
}],
}, {
type: 'link',
text: i18n.ts.aboutMisskey,
to: '/about-misskey',
}], ev.currentTarget ?? ev.target, {
align: 'left',
});

View File

@@ -3,12 +3,12 @@
<div class="body">
<div class="top">
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
<button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
<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.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact>
<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">
@@ -17,7 +17,7 @@
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
v-click-anime
v-tooltip.right="i18n.ts[navbarItemDef[item].title]"
v-tooltip.noDelay.right="i18n.ts[navbarItemDef[item].title]"
class="item _button"
:class="[item, { active: navbarItemDef[item].active }]"
active-class="active"
@@ -29,22 +29,22 @@
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin">
<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.right="i18n.ts.settings" class="item" active-class="active" to="/settings">
<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.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post">
<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.right="i18n.ts.account" class="item _button account" @click="openAccountMenu">
<button v-click-anime v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button>
</div>
@@ -110,6 +110,36 @@ function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.federation,
icon: 'fas fa-globe',
to: '/about#federation',
}, null, {
type: 'parent',
text: i18n.ts.help,
icon: 'fas fa-question-circle',
children: [{
type: 'link',
to: '/mfm-cheat-sheet',
text: i18n.ts._mfm.cheatSheet,
icon: 'fas fa-code',
}, {
type: 'link',
to: '/scratchpad',
text: i18n.ts.scratchpad,
icon: 'fas fa-terminal',
}, {
type: 'link',
to: '/api-console',
text: 'API Console',
icon: 'fas fa-terminal',
}, null, {
text: i18n.ts.document,
icon: 'fas fa-question-circle',
action: () => {
window.open('https://misskey-hub.net/help.html', '_blank');
},
}],
}, {
type: 'link',
text: i18n.ts.aboutMisskey,
to: '/about-misskey',
}], ev.currentTarget ?? ev.target, {
align: 'left',
});
@@ -126,7 +156,7 @@ function more(ev: MouseEvent) {
<style lang="scss" scoped>
.mvcprjjd {
$nav-width: 250px;
$nav-icon-only-width: 86px;
$nav-icon-only-width: 80px;
flex: 0 0 $nav-width;
width: $nav-width;
@@ -356,7 +386,7 @@ function more(ev: MouseEvent) {
> .icon {
display: inline-block;
width: 38px;
width: 30px;
aspect-ratio: 1;
}
}

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,7 +75,7 @@
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_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
@@ -78,9 +86,11 @@ 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 => {
mainRouter.navHook = (path, flag): boolean => {
if (flag === 'forcePage') return false;
const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
if (deckStore.state.navWindow || noMainColumn) {
os.pageWindow(path);
@@ -152,8 +162,6 @@ const onContextmenu = (ev) => {
}], ev);
};
provide('shouldSpacerMin', true);
document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.scrollBehavior = 'auto';
window.addEventListener('wheel', (ev) => {
@@ -168,6 +176,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 +324,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;
}
}
}

View File

@@ -1,5 +1,5 @@
<template>
<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
@@ -11,9 +11,9 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
import { updateColumn, Column } from './deck-store';
import { i18n } from '@/i18n';
const props = defineProps<{
@@ -39,15 +39,22 @@ async function setAntenna() {
const { canceled, result: antenna } = await os.select({
title: i18n.ts.selectAntenna,
items: antennas.map(x => ({
value: x, text: x.name
value: x, text: x.name,
})),
default: props.column.antennaId
default: props.column.antennaId,
});
if (canceled) return;
updateColumn(props.column.id, {
antennaId: antenna.id
antennaId: antenna.id,
});
}
const menu = [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.selectAntenna,
action: setAntenna,
}];
/*
function focus() {
timeline.focus();

View File

@@ -31,31 +31,25 @@
</section>
</template>
<script lang="ts">
export type DeckFunc = {
title: string;
handler: (payload: MouseEvent) => void;
icon?: string;
};
</script>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
import { onBeforeUnmount, onMounted, provide, Ref, watch } from 'vue';
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { MenuItem } from '@/types/menu';
provide('shouldHeaderThin', true);
provide('shouldOmitHeaderTitle', true);
provide('shouldSpacerMin', true);
const props = withDefaults(defineProps<{
column: Column;
isStacked?: boolean;
func?: DeckFunc | null;
naked?: boolean;
indicated?: boolean;
menu?: MenuItem[];
}>(), {
isStacked: false,
func: null,
naked: false,
indicated: false,
});
@@ -110,9 +104,9 @@ function toggleActive() {
}
function getMenu() {
const items = [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.edit,
let items = [{
icon: 'fas fa-cog',
text: i18n.ts._deck.configureColumn,
action: async () => {
const { canceled, result } = await os.form(props.column.name, {
name: {
@@ -134,31 +128,36 @@ function getMenu() {
if (canceled) return;
updateColumn(props.column.id, result);
},
}, null, {
icon: 'fas fa-arrow-left',
text: i18n.ts._deck.swapLeft,
action: () => {
swapLeftColumn(props.column.id);
},
}, {
icon: 'fas fa-arrow-right',
text: i18n.ts._deck.swapRight,
action: () => {
swapRightColumn(props.column.id);
},
}, props.isStacked ? {
icon: 'fas fa-arrow-up',
text: i18n.ts._deck.swapUp,
action: () => {
swapUpColumn(props.column.id);
},
} : undefined, props.isStacked ? {
icon: 'fas fa-arrow-down',
text: i18n.ts._deck.swapDown,
action: () => {
swapDownColumn(props.column.id);
},
} : undefined, null, {
type: 'parent',
text: i18n.ts.move + '...',
icon: 'fas fa-arrows-up-down-left-right',
children: [{
icon: 'fas fa-arrow-left',
text: i18n.ts._deck.swapLeft,
action: () => {
swapLeftColumn(props.column.id);
},
}, {
icon: 'fas fa-arrow-right',
text: i18n.ts._deck.swapRight,
action: () => {
swapRightColumn(props.column.id);
},
}, props.isStacked ? {
icon: 'fas fa-arrow-up',
text: i18n.ts._deck.swapUp,
action: () => {
swapUpColumn(props.column.id);
},
} : undefined, props.isStacked ? {
icon: 'fas fa-arrow-down',
text: i18n.ts._deck.swapDown,
action: () => {
swapDownColumn(props.column.id);
},
} : undefined],
}, {
icon: 'fas fa-window-restore',
text: i18n.ts._deck.stackLeft,
action: () => {
@@ -179,13 +178,9 @@ function getMenu() {
},
}];
if (props.func) {
if (props.menu) {
items.unshift(null);
items.unshift({
icon: props.func.icon,
text: props.func.title,
action: props.func.handler,
});
items = props.menu.concat(items);
}
return items;

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

@@ -1,5 +1,5 @@
<template>
<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
@@ -11,9 +11,9 @@
<script lang="ts" setup>
import { } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
import { updateColumn, Column } from './deck-store';
import { i18n } from '@/i18n';
const props = defineProps<{
@@ -37,16 +37,22 @@ async function setList() {
const { canceled, result: list } = await os.select({
title: i18n.ts.selectList,
items: lists.map(x => ({
value: x, text: x.name
value: x, text: x.name,
})),
default: props.column.listId
default: props.column.listId,
});
if (canceled) return;
updateColumn(props.column.id, {
listId: list.id
listId: list.id,
});
}
const menu = [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.selectList,
action: setList,
}];
/*
function focus() {
timeline.focus();

View File

@@ -1,5 +1,5 @@
<template>
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
<XColumn :column="column" :is-stacked="isStacked" :menu="menu" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
<XNotifications :include-types="column.includingTypes"/>
@@ -9,10 +9,10 @@
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import XColumn from './column.vue';
import { updateColumn , Column } from './deck-store';
import XNotifications from '@/components/notifications.vue';
import * as os from '@/os';
import { updateColumn } from './deck-store';
import { Column } from './deck-store';
import { i18n } from '@/i18n';
const props = defineProps<{
column: Column;
@@ -30,9 +30,15 @@ function func() {
done: async (res) => {
const { includingTypes } = res;
updateColumn(props.column.id, {
includingTypes: includingTypes
includingTypes: includingTypes,
});
},
}, 'closed');
}
const menu = [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.notificationSetting,
action: func,
}];
</script>

View File

@@ -1,5 +1,5 @@
<template>
<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<i v-if="column.tl === 'home'" class="fas fa-home"></i>
<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
@@ -22,9 +22,9 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import XColumn from './column.vue';
import { removeColumn, updateColumn, Column } from './deck-store';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
import { removeColumn, updateColumn, Column } from './deck-store';
import { $i } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
@@ -57,13 +57,13 @@ async function setType() {
const { canceled, result: src } = await os.select({
title: i18n.ts.timeline,
items: [{
value: 'home' as const, text: i18n.ts._timelines.home
value: 'home' as const, text: i18n.ts._timelines.home,
}, {
value: 'local' as const, text: i18n.ts._timelines.local
value: 'local' as const, text: i18n.ts._timelines.local,
}, {
value: 'social' as const, text: i18n.ts._timelines.social
value: 'social' as const, text: i18n.ts._timelines.social,
}, {
value: 'global' as const, text: i18n.ts._timelines.global
value: 'global' as const, text: i18n.ts._timelines.global,
}],
});
if (canceled) {
@@ -73,7 +73,7 @@ async function setType() {
return;
}
updateColumn(props.column.id, {
tl: src
tl: src,
});
}
@@ -97,6 +97,12 @@ function onChangeActiveState(state) {
}
}
const menu = [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.timeline,
action: setType,
}];
/*
export default defineComponent({
watch: {

View File

@@ -1,5 +1,5 @@
<template>
<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
<div class="wtdtxvec">
@@ -46,6 +46,12 @@ function updateWidgets(widgets) {
function func() {
edit = !edit;
}
const menu = [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.editWidgets,
action: func,
}];
</script>
<style lang="scss" scoped>

View File

@@ -600,10 +600,10 @@
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
"@vitejs/plugin-vue@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.0.tgz#7081e2b3fbe04e291bb85107b9fb57a1fa5e6aeb"
integrity sha512-yWP34ArFh/jAeNUDkkLz/kVRLjf5ppJiq4L36f64Cp6dIrMQeYZGDP9xxdemlXfZR9ylN9JgHUl3GzfqOtgYDg==
"@vitejs/plugin-vue@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.1.tgz#b6af8f782485374bbb5fe09edf067a845bf4caae"
integrity sha512-Ll9JgxG7ONIz/XZv3dssfoMUDu9qAnlJ+km+pBA0teYSXzwPCIzS/e1bmwNYl5dcQGs677D21amgfYAnzMl17A==
"@vue/compiler-core@3.2.37":
version "3.2.37"
@@ -4222,10 +4222,10 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vite@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.0.tgz#b4675cb9ab83ec0803b9c952ffa6519bcce033a7"
integrity sha512-M7phQhY3+fRZa0H+1WzI6N+/onruwPTBTMvaj7TzgZ0v2TE+N2sdLKxJOfOv9CckDWt5C4HmyQP81xB4dwRKzA==
vite@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.2.tgz#2a7b4642c53ae066cf724e7e581d6c1fd24e2c32"
integrity sha512-TAqydxW/w0U5AoL5AsD9DApTvGb2iNbGs3sN4u2VdT1GFkQVUfgUldt+t08TZgi23uIauh1TUOQJALduo9GXqw==
dependencies:
esbuild "^0.14.47"
postcss "^8.4.14"