Compare commits
258 Commits
pr/ytplaye
...
develop
Author | SHA1 | Date | |
---|---|---|---|
5e529eb605 | |||
890bef35eb | |||
8aa3787ffc | |||
2b05de0204 | |||
6cc945b53c | |||
c60db40175 | |||
fbc1ec67cf | |||
f66626e6e6 | |||
c7b6e0cbf9 | |||
0a83ec26c8 | |||
bd5712581a | |||
677f6bd6e1 | |||
ea6f538109 | |||
3a540dd1f6 | |||
2be42c0e4b | |||
38e8d2cdb5 | |||
3d8c8af0e8 | |||
d06e9e7e0c | |||
8bc3406c3c | |||
f817979d22 | |||
123fedb5c4 | |||
d01fde57ae | |||
657959dd30 | |||
ac50efc5d1 | |||
1e17b7db6b | |||
e0cea2facc | |||
167a7201c2 | |||
581738f264 | |||
cb18ce16a0 | |||
356d28928a | |||
3bd7f3d301 | |||
fa7c8024d6 | |||
5bff0cb41f | |||
6de468573a | |||
6fb825afdc | |||
f15e9b1e4e | |||
f97146be98 | |||
09ff9f76d0 | |||
95ec171479 | |||
177ee5645e | |||
98a768c28f | |||
a1c97e6700 | |||
41c0505043 | |||
ade3b8e3fb | |||
7817bfac2e | |||
b59e0cda52 | |||
c366a9aa48 | |||
|
a6287d4436 | ||
99a028facf | |||
|
8f00846e44 | ||
|
d8a4837df6 | ||
|
070d91e16e | ||
|
18f778a335 | ||
|
9c48c97047 | ||
|
3a7fbf9406 | ||
c56f8fd953 | |||
9f6e6ac3c8 | |||
35659b6a6a | |||
542349855e | |||
b73d50db29 | |||
d20b109d61 | |||
72983ff9e1 | |||
b1ad672df8 | |||
38ae0c4c1e | |||
bf781707c5 | |||
6214d65ff5 | |||
2d4dfc5137 | |||
2ea8e811d5 | |||
a151a14632 | |||
d93c88e179 | |||
62cfc4ce13 | |||
15a8ec6e45 | |||
8bc0b8e53d | |||
049be7caab | |||
0ed40b920b | |||
3b28e7029c | |||
781dcdef53 | |||
42a7eccb36 | |||
38a53b95ec | |||
8d1f90038b | |||
65bb111a90 | |||
ed9cd9a770 | |||
eb8ab56f46 | |||
6dbd94ef09 | |||
57d53d5bc6 | |||
da16dfa7c9 | |||
fa3dee9222 | |||
5f10f76488 | |||
c78de87c7a | |||
263ac2d462 | |||
adf755f0b2 | |||
de633f0117 | |||
e47495502d | |||
794c2c05a3 | |||
baddc3b6c7 | |||
3c140b2a62 | |||
b72d8e0d58 | |||
c1e56a00e0 | |||
8c857daee0 | |||
65ca1dd106 | |||
b236cfebd3 | |||
f5cf56862d | |||
1e45e9c1a1 | |||
e475790710 | |||
b708b18004 | |||
14cb8262c1 | |||
11127ae87a | |||
8db8fc605f | |||
4c71d05f51 | |||
da8cb8fc73 | |||
aecc01fd8a | |||
ffeb109afd | |||
042d5f8c2d | |||
39f2ccf8a3 | |||
55f0e23a74 | |||
b4edc40fa8 | |||
562e4fd743 | |||
db7abd17fb | |||
fb1781ca53 | |||
17af085bef | |||
5d23cf8c58 | |||
e051b7719d | |||
522e148146 | |||
04c7dcd201 | |||
744310dc4e | |||
7d62342477 | |||
025dc3d6f9 | |||
98f412a172 | |||
970f373cae | |||
c8331b5822 | |||
a8337b0964 | |||
85cc716781 | |||
14990a1bd1 | |||
6b9f0d800e | |||
3e96cc2b6b | |||
5dad50767e | |||
b54e63f835 | |||
d59a072de7 | |||
5e3554d693 | |||
8b3e7bf04c | |||
15a1f1e585 | |||
8db5ec2919 | |||
c7e1bdf701 | |||
4d1b6b1c24 | |||
7a2c54cbd9 | |||
3a896e220a | |||
3499ba5456 | |||
|
641a565a86 | ||
300a84f3c4 | |||
545671818a | |||
22b647d97d | |||
22312ce983 | |||
49b21efb60 | |||
dca6d23415 | |||
7156bfb01c | |||
d4e9082941 | |||
a2242261db | |||
9cf6561a95 | |||
a989e904cc | |||
7774465274 | |||
b2299e1344 | |||
af0dfb2362 | |||
ad74fbc48f | |||
d0775da7d2 | |||
633994ecb6 | |||
54923b7ac9 | |||
48e132a2c7 | |||
7663170fae | |||
cebb66cc36 | |||
f6dda604dc | |||
d10143544d | |||
40e3c75c13 | |||
a2150b1269 | |||
19579ed0f1 | |||
78396baf82 | |||
982896f7be | |||
1c9b842983 | |||
|
0cf950bb35 | ||
ef4f49778e | |||
721c47a74b | |||
11cbbeb795 | |||
|
6bc893ca44 | ||
9c34f96cdd | |||
74a80b3405 | |||
84da9d4cd4 | |||
b48f76aad1 | |||
78b9550eca | |||
8fc770d371 | |||
4aed68ec96 | |||
90c89c3083 | |||
c63d7dc898 | |||
74e695a320 | |||
f269ceaee9 | |||
57a106a192 | |||
5563d6bdeb | |||
ab59a5b659 | |||
a81f8b8275 | |||
|
c99c4b73f4 | ||
|
995d8f2f5d | ||
6bb22007d8 | |||
a4a0d33348 | |||
7d1f3cd1e8 | |||
dc13627a68 | |||
b96e886a61 | |||
7024f6651f | |||
4f9aca0aa3 | |||
88a2606884 | |||
d6b15add1b | |||
39307694b5 | |||
9b7be06eef | |||
5afb3a59cd | |||
73d66fc439 | |||
12214c473a | |||
8e173a6d8e | |||
9d0c1a8fea | |||
aee5193ef6 | |||
2892a31142 | |||
90bb0cec37 | |||
|
19775507fc | ||
9d4f621833 | |||
|
2e095b4863 | ||
|
5d9b9eeaad | ||
98f5cb6cd2 | |||
b88c4d6d34 | |||
6238ba965b | |||
|
8489c3ca7b | ||
|
33871f3cb8 | ||
5eb758fa28 | |||
|
56d8f7f257 | ||
74c3e6d483 | |||
ead62a538f | |||
|
72058b30c2 | ||
|
22102639a8 | ||
|
ee4289076f | ||
|
be011d3985 | ||
|
6ef86df368 | ||
|
34e099984f | ||
|
5a1c6be944 | ||
|
25991cfdc1 | ||
b1a526aeea | |||
|
f31ef70ce7 | ||
09a751b992 | |||
a41e4ed4fd | |||
|
b77f7f5041 | ||
|
7adcf99865 | ||
|
cc2d5480f3 | ||
|
fdd129fb83 | ||
|
8dd3fef0e2 | ||
|
f1c66f09e1 | ||
|
6f328f2ccf | ||
|
70b3d598c6 | ||
|
668c403ece | ||
|
c873f17080 | ||
|
3f6691bd5e | ||
|
260ece9881 | ||
|
9a4c04fe79 | ||
|
24896d4a36 | ||
|
8aea52e9b4 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -45,3 +45,5 @@ ormconfig.json
|
|||||||
*.blend3
|
*.blend3
|
||||||
*.blend4
|
*.blend4
|
||||||
*.blend5
|
*.blend5
|
||||||
|
|
||||||
|
start.sh
|
@ -23,6 +23,7 @@ const languages = [
|
|||||||
'fr-FR',
|
'fr-FR',
|
||||||
'id-ID',
|
'id-ID',
|
||||||
'it-IT',
|
'it-IT',
|
||||||
|
'ja-NY',
|
||||||
'ja-JP',
|
'ja-JP',
|
||||||
'ja-KS',
|
'ja-KS',
|
||||||
'kab-KAB',
|
'kab-KAB',
|
||||||
@ -56,10 +57,12 @@ module.exports = Object.entries(locales)
|
|||||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||||
const [lang] = k.split('-');
|
const [lang] = k.split('-');
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case 'ja-JP': return v;
|
case 'ja-NY': return merge(locales['ja-JP'], v);
|
||||||
|
case 'ja-JP':
|
||||||
case 'ja-KS':
|
case 'ja-KS':
|
||||||
case 'en-US': return merge(locales['ja-JP'], v);
|
case 'en-US': return merge(locales['ja-JP'], v);
|
||||||
default: return merge(
|
default: return merge(
|
||||||
|
locales['ja-NY'],
|
||||||
locales['ja-JP'],
|
locales['ja-JP'],
|
||||||
locales['en-US'],
|
locales['en-US'],
|
||||||
locales[`${lang}-${primaries[lang]}`] || {},
|
locales[`${lang}-${primaries[lang]}`] || {},
|
||||||
|
@ -107,6 +107,7 @@ clickToShow: "クリックして表示"
|
|||||||
sensitive: "閲覧注意"
|
sensitive: "閲覧注意"
|
||||||
add: "追加"
|
add: "追加"
|
||||||
reaction: "リアクション"
|
reaction: "リアクション"
|
||||||
|
reactWithRenote: "ついでにRenoteする"
|
||||||
reactionSetting: "ピッカーに表示するリアクション"
|
reactionSetting: "ピッカーに表示するリアクション"
|
||||||
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
|
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
|
||||||
rememberNoteVisibility: "公開範囲を記憶する"
|
rememberNoteVisibility: "公開範囲を記憶する"
|
||||||
@ -224,6 +225,7 @@ currentPassword: "現在のパスワード"
|
|||||||
newPassword: "新しいパスワード"
|
newPassword: "新しいパスワード"
|
||||||
newPasswordRetype: "新しいパスワード(再入力)"
|
newPasswordRetype: "新しいパスワード(再入力)"
|
||||||
attachFile: "ファイルを添付"
|
attachFile: "ファイルを添付"
|
||||||
|
attachVideoFile: "MOVファイルを添付"
|
||||||
more: "もっと!"
|
more: "もっと!"
|
||||||
featured: "ハイライト"
|
featured: "ハイライト"
|
||||||
usernameOrUserId: "ユーザー名かユーザーID"
|
usernameOrUserId: "ユーザー名かユーザーID"
|
||||||
@ -893,6 +895,10 @@ navbar: "ナビゲーションバー"
|
|||||||
shuffle: "シャッフル"
|
shuffle: "シャッフル"
|
||||||
account: "アカウント"
|
account: "アカウント"
|
||||||
move: "移動"
|
move: "移動"
|
||||||
|
deckOld: "旧デッキ"
|
||||||
|
pakuruConfirm: "パクりますか?"
|
||||||
|
pakuru: "パクる"
|
||||||
|
duplicateEmoji: "同じ名前の絵文字が存在します。インポートしますか?"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||||
@ -1230,6 +1236,10 @@ _tutorial:
|
|||||||
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
||||||
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
|
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
|
||||||
step7_3: "では、Misskeyをお楽しみください🚀"
|
step7_3: "では、Misskeyをお楽しみください🚀"
|
||||||
|
step8_1: "さいごに"
|
||||||
|
step8_2: "このインスタンスではガラス風デザインを標準で採用しています。"
|
||||||
|
step8_3: "端末によってはパフォーマンスが低下する場合があります。"
|
||||||
|
step8_4: "その場合は設定よりブラーを使用しないように設定+テーマをNoGlassにしてください。"
|
||||||
|
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "既に設定は完了しています。"
|
alreadyRegistered: "既に設定は完了しています。"
|
||||||
|
1811
locales/ja-NY.yml
Normal file
1811
locales/ja-NY.yml
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "12.119.0",
|
"version": "12.119.0-simkey-v10111",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/sim1222/misskey.git"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
export const MAX_NOTE_TEXT_LENGTH = 8192;
|
||||||
|
|
||||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
||||||
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
|
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
|
||||||
|
@ -13,7 +13,7 @@ export const meta = {
|
|||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: 60000,
|
duration: 60000,
|
||||||
max: 15,
|
max: 100,
|
||||||
},
|
},
|
||||||
|
|
||||||
kind: 'read:notifications',
|
kind: 'read:notifications',
|
||||||
|
@ -34,6 +34,9 @@
|
|||||||
if (lang == null || !supportedLangs.includes(lang)) {
|
if (lang == null || !supportedLangs.includes(lang)) {
|
||||||
if (supportedLangs.includes(navigator.language)) {
|
if (supportedLangs.includes(navigator.language)) {
|
||||||
lang = navigator.language;
|
lang = navigator.language;
|
||||||
|
if (lang === 'ja-JP') {
|
||||||
|
lang = 'ja-NY';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#313a42",
|
"background_color": "#313a42",
|
||||||
"theme_color": "#86b300",
|
"theme_color": "#a7dc4e",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static-assets/icons/192.png",
|
"src": "/static-assets/icons/192.png",
|
||||||
|
@ -138,7 +138,7 @@ describe('Note', () => {
|
|||||||
|
|
||||||
it('文字数ぎりぎりで怒られない', async(async () => {
|
it('文字数ぎりぎりで怒られない', async(async () => {
|
||||||
const post = {
|
const post = {
|
||||||
text: '!'.repeat(3000),
|
text: '!'.repeat(8192),
|
||||||
};
|
};
|
||||||
const res = await request('/notes/create', post, alice);
|
const res = await request('/notes/create', post, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
@ -146,7 +146,7 @@ describe('Note', () => {
|
|||||||
|
|
||||||
it('文字数オーバーで怒られる', async(async () => {
|
it('文字数オーバーで怒られる', async(async () => {
|
||||||
const post = {
|
const post = {
|
||||||
text: '!'.repeat(3001),
|
text: '!'.repeat(8193),
|
||||||
};
|
};
|
||||||
const res = await request('/notes/create', post, alice);
|
const res = await request('/notes/create', post, alice);
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"katex": "0.15.6",
|
"katex": "0.15.6",
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
"mfm-js": "0.23.0",
|
"mfm-js": "0.23.0",
|
||||||
|
"misetehoshii": "https://github.com/melt-adzuki/misetehoshii",
|
||||||
"misskey-js": "0.0.14",
|
"misskey-js": "0.0.14",
|
||||||
"photoswipe": "5.3.2",
|
"photoswipe": "5.3.2",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
|
@ -71,15 +71,18 @@ function onMousedown(evt: Event) {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.nvlagfpb {
|
.nvlagfpb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), height 0.2s ease-out, transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
transform-origin: left top;
|
transform-origin: left top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from, .fade-leave-to {
|
.fade-enter-from, .fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
|
height: 1px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="fas fa-leaf fa-fw"></i></button>
|
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="fas fa-leaf fa-fw"></i></button>
|
||||||
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="fas fa-hashtag fa-fw"></i></button>
|
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="fas fa-hashtag fa-fw"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
<MkSwitch v-if="props.asReactionPicker" v-model="withRenote" class="withRenote">{{ i18n.ts.reactWithRenote }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -90,6 +91,7 @@ import { deviceKind } from '@/scripts/device-kind';
|
|||||||
import { emojiCategories, instance } from '@/instance';
|
import { emojiCategories, instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
import MkSwitch from '@/components/form/switch.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
@ -101,10 +103,11 @@ const props = withDefaults(defineProps<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string): void;
|
(ev: 'chosen', v: { reaction: string, withRenote: boolean } | string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const search = ref<HTMLInputElement>();
|
const search = ref<HTMLInputElement>();
|
||||||
|
const withRenote = ref<boolean>(true);
|
||||||
const emojis = ref<HTMLDivElement>();
|
const emojis = ref<HTMLDivElement>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -278,6 +281,7 @@ function focus() {
|
|||||||
function reset() {
|
function reset() {
|
||||||
if (emojis.value) emojis.value.scrollTop = 0;
|
if (emojis.value) emojis.value.scrollTop = 0;
|
||||||
q.value = '';
|
q.value = '';
|
||||||
|
withRenote.value = false; // 毎回Renoteをfalseに戻す
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
|
function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
|
||||||
@ -294,7 +298,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = getKey(emoji);
|
const key = getKey(emoji);
|
||||||
emit('chosen', key);
|
emit('chosen', (props.asReactionPicker) ? { reaction: key, withRenote: withRenote.value } : key);
|
||||||
|
|
||||||
// 最近使った絵文字更新
|
// 最近使った絵文字更新
|
||||||
if (!pinned.value.includes(key)) {
|
if (!pinned.value.includes(key)) {
|
||||||
@ -452,6 +456,10 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .withRenote {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
> .tabs {
|
> .tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -51,8 +51,8 @@ const emit = defineEmits<{
|
|||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = ref<InstanceType<typeof MkModal>>();
|
||||||
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
|
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
|
||||||
|
|
||||||
function chosen(emoji: any) {
|
function chosen(results: { reaction: string, withRenote: boolean }) {
|
||||||
emit('done', emoji);
|
emit('done', results);
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,6 +374,8 @@ defineExpose({
|
|||||||
&.popup {
|
&.popup {
|
||||||
> .content {
|
> .content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
&.fixed {
|
&.fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -207,11 +207,17 @@ function reply(viaKeyboard = false): void {
|
|||||||
function react(viaKeyboard = false): void {
|
function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, results => {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: results.reaction,
|
||||||
});
|
});
|
||||||
|
if (results.withRenote) {
|
||||||
|
os.api('notes/create', {
|
||||||
|
renoteId: appearNote.id,
|
||||||
|
isRenote: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
@ -215,7 +215,7 @@ function react(viaKeyboard = false): void {
|
|||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction.reaction,
|
||||||
});
|
});
|
||||||
}, () => {
|
}, () => {
|
||||||
focus();
|
focus();
|
||||||
|
@ -141,4 +141,7 @@ defineExpose({
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
.yrolvcoq:has(.fill) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -27,6 +27,8 @@ let modal = $ref<InstanceType<typeof MkModal>>();
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.sfhdhdhq {
|
.sfhdhdhq {
|
||||||
|
// backdrop-filter: var(--blur, blur(8px));
|
||||||
|
// -webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
&.drawer {
|
&.drawer {
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
|
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
|
||||||
<footer>
|
<footer>
|
||||||
<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
|
<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
|
||||||
|
<button v-tooltip="i18n.ts.attachVideoFile" class="_button" @click="chooseFileFromEnc"><i class="far fa-file-video"></i></button>
|
||||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
|
<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
|
||||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
|
<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
|
||||||
<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
|
<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
|
||||||
@ -83,6 +84,7 @@ import { Autocomplete } from '@/scripts/autocomplete';
|
|||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import { selectFiles } from '@/scripts/select-file';
|
import { selectFiles } from '@/scripts/select-file';
|
||||||
|
import {selectFileEnc, selectFilesEnc} from '@/scripts/select-file-enc';
|
||||||
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
|
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
@ -356,6 +358,12 @@ function chooseFileFrom(ev) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseFileFromEnc(ev) {
|
||||||
|
selectFileEnc(ev.currentTarget ?? ev.target, i18n.ts.attachVideoFile).then(files_ => {
|
||||||
|
files.push(files_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function detachFile(id) {
|
function detachFile(id) {
|
||||||
files = files.filter(x => x.id !== id);
|
files = files.filter(x => x.id !== id);
|
||||||
}
|
}
|
||||||
@ -902,6 +910,7 @@ onMounted(() => {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
min-height: 90px;
|
min-height: 90px;
|
||||||
|
height: 250px;
|
||||||
|
|
||||||
&.withCw {
|
&.withCw {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
class="hkzvhatu _button"
|
class="hkzvhatu _button"
|
||||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
|
@contextmenu.stop="onContextmenu"
|
||||||
>
|
>
|
||||||
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
|
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
|
||||||
<span class="count">{{ count }}</span>
|
<span class="count">{{ count }}</span>
|
||||||
@ -13,13 +14,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
import { openReactionImportMenu } from '@/scripts/reactionImportMenu';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
@ -88,6 +90,11 @@ useTooltip(buttonRef, async (showing) => {
|
|||||||
targetElement: buttonRef.value,
|
targetElement: buttonRef.value,
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
const onContextmenu = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openReactionImportMenu(e, props.reaction);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||||
|
<div class="_formBlock">過去にこのインスタンスを訪れたことがある場合は、<a href="/flush" target="_blank" class="_link">Local Storageを削除</a>してください</div>
|
||||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
|
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
|
||||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||||
<template #prefix><i class="fas fa-key"></i></template>
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||||
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||||
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||||
<button v-if="!playerEnabled && player.url" class="_button" :title="i18n.ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button>
|
<button v-if="!playerEnabled && player.url" class="_button" :title="i18n.ts.enablePlayer" @click.prevent="isMobile? playerEnabled = true : openPlayer()"><i class="fas fa-play-circle"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
@ -36,6 +36,8 @@
|
|||||||
import { onMounted, onUnmounted } from 'vue';
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
import { url as local, lang } from '@/config';
|
import { url as local, lang } from '@/config';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { deviceKind } from '@/scripts/device-kind';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
url: string;
|
url: string;
|
||||||
@ -46,6 +48,9 @@ const props = withDefaults(defineProps<{
|
|||||||
compact: false,
|
compact: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MOBILE_THRESHOLD = 500;
|
||||||
|
const isMobile = $ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
|
||||||
|
|
||||||
const self = props.url.startsWith(local);
|
const self = props.url.startsWith(local);
|
||||||
const attr = self ? 'to' : 'href';
|
const attr = self ? 'to' : 'href';
|
||||||
const target = self ? null : '_blank';
|
const target = self ? null : '_blank';
|
||||||
@ -103,6 +108,10 @@ function adjustTweetHeight(message: any) {
|
|||||||
if (height) tweetHeight = height;
|
if (height) tweetHeight = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openPlayer = () => {
|
||||||
|
os.pageWindow(`/ytplayer/${encodeURIComponent(requestUrl.href)}`);
|
||||||
|
};
|
||||||
|
|
||||||
(window as any).addEventListener('message', adjustTweetHeight);
|
(window as any).addEventListener('message', adjustTweetHeight);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
@ -83,11 +83,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.popup-enter-active, .popup-leave-active {
|
.popup-enter-active, .popup-leave-active {
|
||||||
transition: opacity 0.3s, transform 0.3s !important;
|
transition: opacity 0.3s, height 0.2s, transform 0.3s !important;
|
||||||
}
|
}
|
||||||
.popup-enter-from, .popup-leave-to {
|
.popup-enter-from, .popup-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fxxzrfni {
|
.fxxzrfni {
|
||||||
|
@ -413,6 +413,8 @@ defineExpose({
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
@ -29,6 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="buttons right">
|
<div class="buttons right">
|
||||||
|
<button v-tooltip.noDelay="i18n.ts.reload" class="_button button" onclick="location.reload();">
|
||||||
|
<i class="fa-solid fa-arrow-rotate-right"></i>
|
||||||
|
</button>
|
||||||
<template v-for="action in actions">
|
<template v-for="action in actions">
|
||||||
<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>
|
<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>
|
</template>
|
||||||
|
@ -172,6 +172,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||||||
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||||
|
ui === 'deckold' ? defineAsyncComponent(() => import('@/ui/deckold.vue')) :
|
||||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||||
defineAsyncComponent(() => import('@/ui/universal.vue')),
|
defineAsyncComponent(() => import('@/ui/universal.vue')),
|
||||||
);
|
);
|
||||||
@ -337,6 +338,10 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||||||
's': search,
|
's': search,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!$i) {
|
||||||
|
localStorage.setItem('wallpaper', 'https://simkey.net/files/c2f30819-64f7-42df-91b6-0e7fb122a413');
|
||||||
|
}
|
||||||
|
|
||||||
if ($i) {
|
if ($i) {
|
||||||
// only add post shortcuts if logged in
|
// only add post shortcuts if logged in
|
||||||
hotkeys['p|n'] = post;
|
hotkeys['p|n'] = post;
|
||||||
|
@ -115,6 +115,13 @@ export const navbarItemDef = reactive({
|
|||||||
localStorage.setItem('ui', 'deck');
|
localStorage.setItem('ui', 'deck');
|
||||||
unisonReload();
|
unisonReload();
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.deckOld,
|
||||||
|
active: ui === 'deckold',
|
||||||
|
action: () => {
|
||||||
|
localStorage.setItem('ui', 'deckold');
|
||||||
|
unisonReload();
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts.classic,
|
text: i18n.ts.classic,
|
||||||
active: ui === 'classic',
|
active: ui === 'classic',
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<div class="_formLinks">
|
<div class="_formLinks">
|
||||||
<FormLink to="https://github.com/misskey-dev/misskey" external>
|
<FormLink to="https://github.com/sim1222/misskey" external>
|
||||||
<template #icon><i class="fas fa-code"></i></template>
|
<template #icon><i class="fas fa-code"></i></template>
|
||||||
{{ i18n.ts._aboutMisskey.source }}
|
{{ i18n.ts._aboutMisskey.source }}
|
||||||
<template #suffix>GitHub</template>
|
<template #suffix>GitHub</template>
|
||||||
|
380
packages/client/src/pages/admin/emojigen.vue
Normal file
380
packages/client/src/pages/admin/emojigen.vue
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><XHeader v-model:tab="tab" :tabs="headerTabs" /></template>
|
||||||
|
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||||
|
<div class="cwepdizn _formRoot">
|
||||||
|
<div v-if="tab === 'string'" class="local">
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.preview }}</template>
|
||||||
|
<p><img :src="previewUrl" class="img" :alt="emojiName" /></p>
|
||||||
|
</FormSection>
|
||||||
|
<FormButton primary class="_formBlock" @click="uploadEmoji('url')">{{ $ts.emojiApproval }}</FormButton>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.settings }}</template>
|
||||||
|
<FormInput v-model="emojiName" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiName }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormTextarea v-model="text" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.text }}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
|
<FormRadios v-model="emojiAlign" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiAlign }}</template>
|
||||||
|
<option value="left"><i class="fas fa-align-left"/></option>
|
||||||
|
<option value="center"><i class="fas fa-align-center"></i></option>
|
||||||
|
<option value="right"><i class="fas fa-align-right"/></option>
|
||||||
|
</FormRadios>
|
||||||
|
|
||||||
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiSizeSetting }}</template>
|
||||||
|
<FormSection>
|
||||||
|
<FormSwitch v-model="emojiSizeFixed" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiSizeFixed }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<FormSwitch v-model="emojiStretch" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiStretch }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
</FormSection>
|
||||||
|
</FormFolder>
|
||||||
|
|
||||||
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._pages.font }}</template>
|
||||||
|
<FormRadios v-model="font" class="_formBlock">
|
||||||
|
<option value="notosans-mono-bold">Noto Sans Mono CJK JP Bold</option>
|
||||||
|
<option value="mplus-1p-black">M+ 1p black</option>
|
||||||
|
<option value="rounded-x-mplus-1p-black">Rounded M+ 1p black</option>
|
||||||
|
<option value="ipamjm">IPAmj明朝</option>
|
||||||
|
<option value="aoyagireisyoshimo">青柳隷書しも</option>
|
||||||
|
<option value="LinLibertine_RBah">LinLibertine Bold</option>
|
||||||
|
</FormRadios>
|
||||||
|
</FormFolder>
|
||||||
|
|
||||||
|
|
||||||
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiColor }}</template>
|
||||||
|
<FormSection>
|
||||||
|
<div class="cwepdizn-colors">
|
||||||
|
<div class="row">
|
||||||
|
<button v-for="color in accentColors" :key="color" class="color rounded _button" @click="setAccentColor(color)">
|
||||||
|
<div class="preview" :style="{ background: color }"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormInput v-model="emojiColor" class="_formBlock" :style="{ color: '#' + emojiColor }">
|
||||||
|
<template #prefix><i class="fas fa-palette"></i></template>
|
||||||
|
<template #label @click="colorPick()">{{ $ts.emojiColor }}</template>
|
||||||
|
<template #caption>#RRGGBB</template>
|
||||||
|
</FormInput>
|
||||||
|
<FormButton @click="colorPick()">{{ $ts.colorPicker }}</FormButton>
|
||||||
|
</FormSection>
|
||||||
|
</FormFolder>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tab === 'misetehoshii'" class="remote">
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.preview }}</template>
|
||||||
|
<canvas class="preview__content" ref="canvas" width="64" height="30" />
|
||||||
|
</FormSection>
|
||||||
|
<FormButton primary class="_formBlock" @click="uploadEmoji('')">{{ $ts.emojiApproval }}</FormButton>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.settings }}</template>
|
||||||
|
<FormInput v-model="emojiName" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiName }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormTextarea v-model="text" value="見せてほしい" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.text }}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
|
<FormRadios v-model="backColor" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._theme.color }}</template>
|
||||||
|
<option v-for="c in backColors" v-bind:value="c">{{c}}</option>
|
||||||
|
</FormRadios>
|
||||||
|
</FormSection>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>見せてほしいメーカー</template>
|
||||||
|
<div>Powered by <MkLink url="https://github.com/melt-adzuki/misetehoshii">misetehoshii</MkLink>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { stream } from '@/stream';
|
||||||
|
import { uploadFile } from '@/scripts/upload';
|
||||||
|
import XHeader from './_header_.vue';
|
||||||
|
import MkTab from '@/components/MkTab.vue';
|
||||||
|
import MkLink from '@/components/MkLink.vue';
|
||||||
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
import FormInput from '@/components/form/input.vue';
|
||||||
|
import FormSwitch from '@/components/form/switch.vue';
|
||||||
|
import FormButton from '@/components/MkButton.vue';
|
||||||
|
import FormRadios from '@/components/form/radios.vue';
|
||||||
|
import FormTextarea from '@/components/form/textarea.vue';
|
||||||
|
import FormFolder from '@/components/form/folder.vue';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { buttonImages } from "misetehoshii/src/assets";
|
||||||
|
import { draw } from "misetehoshii/src/canvas"
|
||||||
|
|
||||||
|
definePageMetadata(computed(() => ({
|
||||||
|
title: i18n.ts.emojiGen,
|
||||||
|
icon: 'fas fa-kiss-wink-heart',
|
||||||
|
})));
|
||||||
|
|
||||||
|
const tab = ref('string');
|
||||||
|
const headerTabs = $computed(() => [{
|
||||||
|
key: 'string',
|
||||||
|
title: 'もじもじ',
|
||||||
|
}, {
|
||||||
|
key: 'misetehoshii',
|
||||||
|
title: '見せてほしい',
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const font = ref('rounded-x-mplus-1p-black');
|
||||||
|
const text = ref('');
|
||||||
|
const emojiName = ref('');
|
||||||
|
const emojiAlign = ref('center');
|
||||||
|
const emojiSizeFixed = ref(false);
|
||||||
|
const emojiStretch = ref(false);
|
||||||
|
const emojiColor = ref('90ee90');
|
||||||
|
const previewUrl = ref('');
|
||||||
|
const accentColors = [
|
||||||
|
'#ff00ff',
|
||||||
|
'#ff0080',
|
||||||
|
'#D55353',
|
||||||
|
'#ff8080',
|
||||||
|
'#ff8040',
|
||||||
|
'#ffff80',
|
||||||
|
'#80ff80',
|
||||||
|
'#90ee90',
|
||||||
|
'#80ffff',
|
||||||
|
'#0080ff',
|
||||||
|
'#8080ff',
|
||||||
|
];
|
||||||
|
|
||||||
|
const backColor = ref('blue')
|
||||||
|
const canvas = ref<HTMLCanvasElement>()
|
||||||
|
const buttonImage = computed(() => buttonImages[backColor.value])
|
||||||
|
const backColors = [
|
||||||
|
'blue',
|
||||||
|
'peacockGreen',
|
||||||
|
'green',
|
||||||
|
'yellow',
|
||||||
|
'red',
|
||||||
|
'pink',
|
||||||
|
'disabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
const updateCanvas = (): void => {
|
||||||
|
const context = canvas.value!.getContext("2d")!
|
||||||
|
draw({ context, text: text.value, button: buttonImage.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([font, text, emojiAlign, emojiSizeFixed, emojiStretch, emojiColor, backColor], () => {
|
||||||
|
preview();
|
||||||
|
updateCanvas();
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorPick = (): void => {
|
||||||
|
const input = document.createElement('input') as HTMLInputElement;
|
||||||
|
input.type = 'color';
|
||||||
|
input.value = `#${emojiColor.value}`;
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
emojiColor.value = input.value.replace('#', '');
|
||||||
|
});
|
||||||
|
(window as any).__misskey_input_ref__ = input;
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeUrl = (): string => {
|
||||||
|
const API_URL = 'https://emoji-gen.ninja/emoji';
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
text: encodeURI(text.value),
|
||||||
|
color: emojiColor.value + 'FF',
|
||||||
|
back_color: '00000000',
|
||||||
|
font: font.value,
|
||||||
|
size_fixed: !emojiSizeFixed.value ? 'true' : 'false',
|
||||||
|
align: 'center',
|
||||||
|
stretch: !emojiStretch.value ? 'true' : 'false',
|
||||||
|
public_fg: 'false',
|
||||||
|
locale: 'ja',
|
||||||
|
};
|
||||||
|
|
||||||
|
return API_URL + '?' + Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&');
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview = (): void => {
|
||||||
|
previewUrl.value = makeUrl();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAccentColor = (color) => {
|
||||||
|
emojiColor.value = color.replace('#', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFileFromUrlWithId = (url: string) => new Promise<string>(async resolve => {
|
||||||
|
const marker = uuid();
|
||||||
|
|
||||||
|
// 先にコネクションを貼り、アップロードが完了したかどうかを監視する
|
||||||
|
const connection = stream.useChannel('main');
|
||||||
|
|
||||||
|
connection.on('urlUploadFinished', async response => {
|
||||||
|
if (response.marker === marker) {
|
||||||
|
resolve(response.file.id);
|
||||||
|
connection.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 絵文字をアップロード
|
||||||
|
await os.api('drive/files/upload-from-url', {
|
||||||
|
url,
|
||||||
|
folderId: defaultStore.state.uploadFolder,
|
||||||
|
marker,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEmojiObject = emojiId => new Promise<Record<string, any> | null>(async resolve => {
|
||||||
|
const sinceId = await os.api('admin/emoji/list', {
|
||||||
|
limit: 1,
|
||||||
|
untilId: emojiId.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sinceId || !sinceId[0] || !sinceId[0].id) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = await os.api('admin/emoji/list', {
|
||||||
|
limit: 1,
|
||||||
|
sinceId: sinceId[0].id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!id || !id[0]) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(id[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadEmoji = async (type: string) => {
|
||||||
|
|
||||||
|
if (type == 'url') {
|
||||||
|
const emojiUrl = makeUrl();
|
||||||
|
const fileId = await uploadFileFromUrlWithId(emojiUrl);
|
||||||
|
|
||||||
|
// ドライブにアップロードされたファイルをリネーム
|
||||||
|
await os.api('drive/files/update', {
|
||||||
|
fileId,
|
||||||
|
name: emojiName.value + '.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiId = await os.api('admin/emoji/add', { fileId });
|
||||||
|
const emoji = await getEmojiObject(emojiId);
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.ts.failedToUploadEmoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { emoji });
|
||||||
|
} else {
|
||||||
|
const canvasImg = new Promise<Blob>(resolve => {
|
||||||
|
canvas.value!.toBlob(blob => resolve(blob?.slice(0, blob.size, 'image/png') as Blob));
|
||||||
|
})
|
||||||
|
const file = new File([await canvasImg], emojiName.value + '.png', { type: 'image/png' });
|
||||||
|
const fileId = (await uploadFile(file, defaultStore.state.uploadFolder, undefined, true)).id
|
||||||
|
|
||||||
|
// ドライブにアップロードされたファイルをリネーム
|
||||||
|
await os.api('drive/files/update', {
|
||||||
|
fileId,
|
||||||
|
name: emojiName.value + '.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiId = await os.api('admin/emoji/add', { fileId });
|
||||||
|
const emoji = await getEmojiObject(emojiId);
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.ts.failedToUploadEmoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { emoji });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.preview-img {
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
|
.cwepdizn {
|
||||||
|
::v-deep(.cwepdizn-colors) {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> .row {
|
||||||
|
> .color {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
> .preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
> .preview {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
box-shadow: 0 0 0 2px var(--divider) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rounded {
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
> .preview {
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.char {
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
|
import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
|
||||||
|
import { $i } from '../../account';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
@ -101,6 +102,11 @@ const menuDef = $computed(() => [{
|
|||||||
text: i18n.ts.customEmojis,
|
text: i18n.ts.customEmojis,
|
||||||
to: '/admin/emojis',
|
to: '/admin/emojis',
|
||||||
active: currentPage?.route.name === 'emojis',
|
active: currentPage?.route.name === 'emojis',
|
||||||
|
}, {
|
||||||
|
icon: 'fas fa-laugh',
|
||||||
|
text: i18n.ts.emojiGen,
|
||||||
|
to: '/admin/emojigen',
|
||||||
|
active: currentPage?.route.name === 'emojigen',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-globe',
|
icon: 'fas fa-globe',
|
||||||
text: i18n.ts.federation,
|
text: i18n.ts.federation,
|
||||||
@ -134,52 +140,52 @@ const menuDef = $computed(() => [{
|
|||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
title: i18n.ts.settings,
|
title: i18n.ts.settings,
|
||||||
items: [{
|
items: [ ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-cog',
|
icon: 'fas fa-cog',
|
||||||
text: i18n.ts.general,
|
text: i18n.ts.general,
|
||||||
to: '/admin/settings',
|
to: '/admin/settings',
|
||||||
active: currentPage?.route.name === 'settings',
|
active: currentPage?.route.name === 'settings',
|
||||||
}, {
|
}] : []), ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-envelope',
|
icon: 'fas fa-envelope',
|
||||||
text: i18n.ts.emailServer,
|
text: i18n.ts.emailServer,
|
||||||
to: '/admin/email-settings',
|
to: '/admin/email-settings',
|
||||||
active: currentPage?.route.name === 'email-settings',
|
active: currentPage?.route.name === 'email-settings',
|
||||||
}, {
|
}] : []), ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-cloud',
|
icon: 'fas fa-cloud',
|
||||||
text: i18n.ts.objectStorage,
|
text: i18n.ts.objectStorage,
|
||||||
to: '/admin/object-storage',
|
to: '/admin/object-storage',
|
||||||
active: currentPage?.route.name === 'object-storage',
|
active: currentPage?.route.name === 'object-storage',
|
||||||
}, {
|
}] : []), ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-lock',
|
icon: 'fas fa-lock',
|
||||||
text: i18n.ts.security,
|
text: i18n.ts.security,
|
||||||
to: '/admin/security',
|
to: '/admin/security',
|
||||||
active: currentPage?.route.name === 'security',
|
active: currentPage?.route.name === 'security',
|
||||||
}, {
|
}] : []), {
|
||||||
icon: 'fas fa-globe',
|
icon: 'fas fa-globe',
|
||||||
text: i18n.ts.relays,
|
text: i18n.ts.relays,
|
||||||
to: '/admin/relays',
|
to: '/admin/relays',
|
||||||
active: currentPage?.route.name === 'relays',
|
active: currentPage?.route.name === 'relays',
|
||||||
}, {
|
}, ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-share-alt',
|
icon: 'fas fa-share-alt',
|
||||||
text: i18n.ts.integration,
|
text: i18n.ts.integration,
|
||||||
to: '/admin/integrations',
|
to: '/admin/integrations',
|
||||||
active: currentPage?.route.name === 'integrations',
|
active: currentPage?.route.name === 'integrations',
|
||||||
}, {
|
}] : []), ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-ban',
|
icon: 'fas fa-ban',
|
||||||
text: i18n.ts.instanceBlocking,
|
text: i18n.ts.instanceBlocking,
|
||||||
to: '/admin/instance-block',
|
to: '/admin/instance-block',
|
||||||
active: currentPage?.route.name === 'instance-block',
|
active: currentPage?.route.name === 'instance-block',
|
||||||
}, {
|
}] : []), ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-ghost',
|
icon: 'fas fa-ghost',
|
||||||
text: i18n.ts.proxyAccount,
|
text: i18n.ts.proxyAccount,
|
||||||
to: '/admin/proxy-account',
|
to: '/admin/proxy-account',
|
||||||
active: currentPage?.route.name === 'proxy-account',
|
active: currentPage?.route.name === 'proxy-account',
|
||||||
}, {
|
}] : []), ...($i?.isAdmin ? [{
|
||||||
icon: 'fas fa-cogs',
|
icon: 'fas fa-cogs',
|
||||||
text: i18n.ts.other,
|
text: i18n.ts.other,
|
||||||
to: '/admin/other-settings',
|
to: '/admin/other-settings',
|
||||||
active: currentPage?.route.name === 'other-settings',
|
active: currentPage?.route.name === 'other-settings',
|
||||||
}],
|
}] : [])],
|
||||||
}, {
|
}, {
|
||||||
title: i18n.ts.info,
|
title: i18n.ts.info,
|
||||||
items: [{
|
items: [{
|
||||||
|
@ -48,33 +48,139 @@
|
|||||||
</I18n>
|
</I18n>
|
||||||
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
|
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="tutorial === 7" class="_content">
|
||||||
|
<div>{{ $ts._tutorial.step8_1 }}</div>
|
||||||
|
<div>{{ $ts._tutorial.step8_2 }}</div>
|
||||||
|
<div>{{ $ts._tutorial.step8_3 }}</div>
|
||||||
|
<div>{{ $ts._tutorial.step8_4 }}</div>
|
||||||
|
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ i18n.ts.useBlurEffect }}</FormSwitch>
|
||||||
|
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch>
|
||||||
|
<template v-if="darkMode">
|
||||||
|
<FormSelect v-model="darkThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForDarkMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-moon"></i></template>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
<FormSelect v-model="lightThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForLightMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-sun"></i></template>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<FormSelect v-model="lightThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForLightMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-sun"></i></template>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
<FormSelect v-model="darkThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForDarkMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-moon"></i></template>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div class="_footer navigation">
|
<div class="_footer navigation">
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
|
<button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
<span>{{ tutorial + 1 }} / 7</span>
|
<span>{{ tutorial + 1 }} / 8</span>
|
||||||
<button class="arrow _button" :disabled="tutorial === 6" @click="tutorial++">
|
<button class="arrow _button" :disabled="tutorial === 7" @click="tutorial++">
|
||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MkButton v-if="tutorial === 6" class="ok" primary @click="tutorial = -1"><i class="fas fa-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
<MkButton v-if="tutorial === 7" class="ok" primary @click="tutorial = -1"><i class="fas fa-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||||
<MkButton v-else class="ok" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
|
<MkButton v-else class="ok" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, onActivated, ref, watch } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { defaultStore } from '@/store';
|
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||||
|
import FormSwitch from '@/components/form/switch.vue';
|
||||||
|
import FormSelect from '@/components/form/select.vue';
|
||||||
|
import { fetchThemes, getThemes } from '@/theme-store';
|
||||||
|
import { getBuiltinThemesRef } from '@/scripts/theme';
|
||||||
|
import { uniqueBy } from '@/scripts/array';
|
||||||
|
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||||
|
|
||||||
const tutorial = computed({
|
const tutorial = computed({
|
||||||
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
||||||
set(value) { defaultStore.set('tutorial', value); },
|
set(value) { defaultStore.set('tutorial', value); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||||
|
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
|
||||||
|
|
||||||
|
const installedThemes = ref(getThemes());
|
||||||
|
const builtinThemes = getBuiltinThemesRef();
|
||||||
|
const instanceThemes = [];
|
||||||
|
|
||||||
|
const themes = computed(() => uniqueBy([...instanceThemes, ...builtinThemes.value, ...installedThemes.value], theme => theme.id));
|
||||||
|
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
||||||
|
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
|
||||||
|
const darkTheme = ColdDeviceStorage.ref('darkTheme');
|
||||||
|
const darkThemeId = computed({
|
||||||
|
get() {
|
||||||
|
return darkTheme.value.id;
|
||||||
|
},
|
||||||
|
set(id) {
|
||||||
|
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lightTheme = ColdDeviceStorage.ref('lightTheme');
|
||||||
|
const lightThemeId = computed({
|
||||||
|
get() {
|
||||||
|
return lightTheme.value.id;
|
||||||
|
},
|
||||||
|
set(id) {
|
||||||
|
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||||
|
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||||
|
|
||||||
|
watch(syncDeviceDarkMode, () => {
|
||||||
|
if (syncDeviceDarkMode.value) {
|
||||||
|
defaultStore.set('darkMode', isDeviceDarkmode());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
fetchThemes().then(() => {
|
||||||
|
installedThemes.value = getThemes();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchThemes().then(() => {
|
||||||
|
installedThemes.value = getThemes();
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -175,7 +175,7 @@ definePageMetadata(computed(() => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .tl {
|
> .tl {
|
||||||
background: var(--bg);
|
// background: var(--bg);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
@ -209,20 +209,34 @@ function showMenu(ev) {
|
|||||||
> .main {
|
> .main {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(480px, 100%);
|
width: min(480px, 100%);
|
||||||
margin: auto auto auto 128px;
|
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
|
box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
|
||||||
|
|
||||||
|
margin: 0 0 auto 0;
|
||||||
|
animation: moveX 5s linear 0s infinite alternate, moveY 6.4s linear 0s infinite alternate;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
|
||||||
|
@keyframes moveX {
|
||||||
|
from { left: 0; } to { left: calc(100vw - 500px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveY {
|
||||||
|
from { top: 0; } to { top: calc(100vh - 510px); }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
margin: auto;
|
margin: 0 0 auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
width: 85px;
|
width: 85px;
|
||||||
margin-top: -47px;
|
margin-top: -47px;
|
||||||
border-radius: 100%;
|
border-radius: 20%;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .menu {
|
> .menu {
|
||||||
@ -306,4 +320,8 @@ function showMenu(ev) {
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
._panel {
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -179,8 +179,21 @@ export default defineComponent({
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
|
||||||
|
margin: 0 0 auto 0;
|
||||||
|
animation: moveX 5s linear 0s infinite alternate, moveY 6.4s linear 0s infinite alternate;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
|
||||||
|
@keyframes moveX {
|
||||||
|
from { left: 0; } to { left: calc(100vw - 500px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveY {
|
||||||
|
from { top: 0; } to { top: calc(100vh - 510px); }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
margin: auto;
|
margin: 0 0 auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> h1 {
|
> h1 {
|
||||||
@ -234,4 +247,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._panel {
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div v-if="meta" class="rsqzvsbo">
|
<div v-if="meta" class="rsqzvsbo">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<MkFeaturedPhotos class="bg"/>
|
<MkFeaturedPhotos class="bg"/>
|
||||||
<div class="fade"></div>
|
<!-- <div class="fade"></div> -->
|
||||||
<div class="emojis">
|
<div class="emojis">
|
||||||
<MkEmoji :normal="true" :no-style="true" emoji="👍"/>
|
<MkEmoji :normal="true" :no-style="true" emoji="👍"/>
|
||||||
<MkEmoji :normal="true" :no-style="true" emoji="❤"/>
|
<MkEmoji :normal="true" :no-style="true" emoji="❤"/>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
|
<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<MkButton inline gradate @click="signup()">{{ $ts.signup }}</MkButton>
|
<MkButton inline gradate class="signup" @click="signup()">{{ $ts.signup }}</MkButton>
|
||||||
<MkButton inline @click="signin()">{{ $ts.login }}</MkButton>
|
<MkButton inline @click="signin()">{{ $ts.login }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="onlineUsersCount && stats" class="status">
|
<div v-if="onlineUsersCount && stats" class="status">
|
||||||
@ -191,7 +191,19 @@ export default defineComponent({
|
|||||||
> .main {
|
> .main {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(460px, 100%);
|
width: min(460px, 100%);
|
||||||
margin: auto;
|
|
||||||
|
margin: 0 0 auto 0;
|
||||||
|
animation: moveX 5s linear 0s infinite alternate, moveY 6.4s linear 0s infinite alternate;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
|
||||||
|
@keyframes moveX {
|
||||||
|
from { left: 0; } to { left: calc(100vw - 500px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveY {
|
||||||
|
from { top: 0; } to { top: calc(100vh - 510px); }
|
||||||
|
}
|
||||||
|
|
||||||
> .misskey {
|
> .misskey {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
@ -303,4 +315,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._panel {
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -88,6 +88,9 @@ export default defineComponent({
|
|||||||
margin: 0 0 0 auto;
|
margin: 0 0 0 auto;
|
||||||
max-width: max-content;
|
max-width: max-content;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
background: var(--panel);
|
||||||
|
|
||||||
> .richcontent {
|
> .richcontent {
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
|
83
packages/client/src/pages/ytplayer.vue
Normal file
83
packages/client/src/pages/ytplayer.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="poamfof fill">
|
||||||
|
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||||
|
<div v-if="player.url" class="player">
|
||||||
|
<iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<MkLoading v-if="fetching" />
|
||||||
|
<MkError v-else-if="!player.url" @retry="ytFetch()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { lang } from '@/config';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const requestUrl = new URL(props.url);
|
||||||
|
|
||||||
|
let fetching = $ref(true);
|
||||||
|
let title = $ref<string | null>(null);
|
||||||
|
let player = $ref({
|
||||||
|
url: null,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
|
||||||
|
|
||||||
|
const ytFetch = () => {
|
||||||
|
fetching = true;
|
||||||
|
fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
|
||||||
|
res.json().then(info => {
|
||||||
|
if (info.url == null) return;
|
||||||
|
title = info.title;
|
||||||
|
fetching = false;
|
||||||
|
player = info.player;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ytFetch();
|
||||||
|
|
||||||
|
definePageMetadata(computed(() => props.url ? {
|
||||||
|
title: title?.toString() || 'Youtube Player',
|
||||||
|
path: `/notes/${props.url}`,
|
||||||
|
icon: 'fa-brands fa-youtube'
|
||||||
|
} : null));
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.poamfof {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 64px;
|
||||||
|
|
||||||
|
.player {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
@ -316,6 +316,10 @@ export const routes = [{
|
|||||||
path: '/emojis',
|
path: '/emojis',
|
||||||
name: 'emojis',
|
name: 'emojis',
|
||||||
component: page(() => import('./pages/admin/emojis.vue')),
|
component: page(() => import('./pages/admin/emojis.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/emojigen',
|
||||||
|
name: 'emojigen',
|
||||||
|
component: page(() => import('./pages/admin/emojigen.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/queue',
|
path: '/queue',
|
||||||
name: 'queue',
|
name: 'queue',
|
||||||
@ -445,6 +449,9 @@ export const routes = [{
|
|||||||
path: '/timeline/antenna/:antennaId',
|
path: '/timeline/antenna/:antennaId',
|
||||||
component: page(() => import('./pages/antenna-timeline.vue')),
|
component: page(() => import('./pages/antenna-timeline.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/ytplayer/:url',
|
||||||
|
component: page(() => import('./pages/ytplayer.vue')),
|
||||||
}, {
|
}, {
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
@ -26,6 +26,22 @@ export function getNoteMenu(props: {
|
|||||||
|
|
||||||
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
|
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
|
||||||
|
|
||||||
|
function pakuru(): void {
|
||||||
|
os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
text: i18n.ts.pakuruConfirm,
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) return;
|
||||||
|
const postData = {
|
||||||
|
text: appearNote.text,
|
||||||
|
cw: appearNote.cw ? appearNote.cw || '' : undefined,
|
||||||
|
localOnly: appearNote.localOnly,
|
||||||
|
visibility: appearNote.visibility,
|
||||||
|
}
|
||||||
|
os.api('notes/create', postData, undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function del(): void {
|
function del(): void {
|
||||||
os.confirm({
|
os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@ -205,6 +221,11 @@ export function getNoteMenu(props: {
|
|||||||
action: unclip,
|
action: unclip,
|
||||||
}, null] : []
|
}, null] : []
|
||||||
),
|
),
|
||||||
|
{
|
||||||
|
icon: 'fas fa-copy',
|
||||||
|
text: i18n.ts.pakuru,
|
||||||
|
action: pakuru,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'fas fa-copy',
|
icon: 'fas fa-copy',
|
||||||
text: i18n.ts.copyContent,
|
text: i18n.ts.copyContent,
|
||||||
|
@ -4,7 +4,7 @@ import { popup } from '@/os';
|
|||||||
class ReactionPicker {
|
class ReactionPicker {
|
||||||
private src: Ref<HTMLElement | null> = ref(null);
|
private src: Ref<HTMLElement | null> = ref(null);
|
||||||
private manualShowing = ref(false);
|
private manualShowing = ref(false);
|
||||||
private onChosen?: (reaction: string) => void;
|
private onChosen?: ({ reaction: string, withRenote: boolean }) => void;
|
||||||
private onClosed?: () => void;
|
private onClosed?: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -17,8 +17,8 @@ class ReactionPicker {
|
|||||||
asReactionPicker: true,
|
asReactionPicker: true,
|
||||||
manualShowing: this.manualShowing
|
manualShowing: this.manualShowing
|
||||||
}, {
|
}, {
|
||||||
done: reaction => {
|
done: results => {
|
||||||
this.onChosen!(reaction);
|
this.onChosen!({ reaction: results.reaction, withRenote: results.withRenote });
|
||||||
},
|
},
|
||||||
close: () => {
|
close: () => {
|
||||||
this.manualShowing.value = false;
|
this.manualShowing.value = false;
|
||||||
|
114
packages/client/src/scripts/reactionImportMenu.ts
Normal file
114
packages/client/src/scripts/reactionImportMenu.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
|
import { MenuItem } from '@/types/menu';
|
||||||
|
|
||||||
|
export async function openReactionImportMenu(ev: MouseEvent, reaction: string): Promise<void> {
|
||||||
|
if (!reaction) return;
|
||||||
|
|
||||||
|
const host = reaction.match(/(?<=@).*\.*(?=:)/g)?.[0];
|
||||||
|
const name = reaction.match(/(?<=:).*(?=@.*\.*(?=:))/g)?.[0];
|
||||||
|
const isLocal = (host === null || host === '.');
|
||||||
|
const isCustom = reaction.startsWith(':');
|
||||||
|
|
||||||
|
const getEmojiObject = (emojiId): Promise<Record<string, any> | null> => new Promise<Record<string, any> | null>(async resolve => {
|
||||||
|
const sinceId = await os.api('admin/emoji/list', {
|
||||||
|
limit: 1,
|
||||||
|
untilId: emojiId.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sinceId || !sinceId[0] || !sinceId[0].id) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = await os.api('admin/emoji/list', {
|
||||||
|
limit: 1,
|
||||||
|
sinceId: sinceId[0].id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!id || !id[0]) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(id[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEmojiId = async (): Promise<string | null> => {
|
||||||
|
if (isLocal) return null;
|
||||||
|
if (!host || !name) return null;
|
||||||
|
|
||||||
|
const resList: Record<string, any>[] = await os.api('admin/emoji/list-remote', {
|
||||||
|
host,
|
||||||
|
query: name,
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiId = await resList.find(emoji => emoji.name === name && emoji.host === host)?.id;
|
||||||
|
|
||||||
|
return emojiId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importEmoji = async (): Promise<void> => {
|
||||||
|
const emojiId = await getEmojiId();
|
||||||
|
if (!await emojiId) return;
|
||||||
|
os.api('admin/emoji/copy', {
|
||||||
|
emojiId: emojiId,
|
||||||
|
}).then(async emoji => os.popup(defineAsyncComponent(() => import('@/pages/admin/emoji-edit-dialog.vue')), {
|
||||||
|
emoji: await getEmojiObject(emoji),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuItems: MenuItem[] = [{
|
||||||
|
type: 'label',
|
||||||
|
text: reaction,
|
||||||
|
}, {
|
||||||
|
type: 'button',
|
||||||
|
icon: 'fas fa-copy',
|
||||||
|
text: i18n.ts.copy,
|
||||||
|
action: (): void => {
|
||||||
|
copyToClipboard(isCustom ? `:${name}:` : reaction);
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
const emojiId = await getEmojiId() ? await getEmojiId() : reaction;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isCustom &&
|
||||||
|
emojiId &&
|
||||||
|
($i?.isAdmin || $i?.isModerator) &&
|
||||||
|
!isLocal
|
||||||
|
) {
|
||||||
|
menuItems.push({
|
||||||
|
type: 'button',
|
||||||
|
icon: 'fas fa-download',
|
||||||
|
text: i18n.ts.import,
|
||||||
|
action: async () => {
|
||||||
|
const duplication: boolean = await os.api('meta').then(meta => {
|
||||||
|
const emojis = meta.emojis;
|
||||||
|
return emojis.some((emoji) => {
|
||||||
|
return (emoji.name === name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await duplication) {
|
||||||
|
os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.duplicateEmoji,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.canceled) return;
|
||||||
|
importEmoji();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
importEmoji();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
os.contextMenu(menuItems, ev);
|
||||||
|
}
|
||||||
|
|
41
packages/client/src/scripts/select-file-enc.ts
Normal file
41
packages/client/src/scripts/select-file-enc.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { DriveFile } from 'misskey-js/built/entities';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { uploadFile } from '@/scripts/upload';
|
||||||
|
|
||||||
|
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||||
|
|
||||||
|
const chooseFileFromPc = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'video/mp4';
|
||||||
|
input.multiple = multiple;
|
||||||
|
input.onchange = () => {
|
||||||
|
const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
|
||||||
|
|
||||||
|
Promise.all(promises).then(driveFiles => {
|
||||||
|
res(multiple ? driveFiles : driveFiles[0]);
|
||||||
|
}).catch(err => {
|
||||||
|
// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
|
||||||
|
});
|
||||||
|
|
||||||
|
// 一応廃棄
|
||||||
|
(window as any).__misskey_input_ref__ = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
|
||||||
|
// iOS Safari で正常に動かす為のおまじない
|
||||||
|
(window as any).__misskey_input_ref__ = input;
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
chooseFileFromPc();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectFileEnc(src: any, label: string | null = null): Promise<DriveFile> {
|
||||||
|
return select(src, label, false) as Promise<DriveFile>;
|
||||||
|
}
|
@ -6,6 +6,7 @@ import { i18n } from '@/i18n';
|
|||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { uploadFile } from '@/scripts/upload';
|
import { uploadFile } from '@/scripts/upload';
|
||||||
|
|
||||||
|
|
||||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||||
|
@ -18,6 +18,8 @@ export const themeProps = Object.keys(lightTheme.props).filter(key => !key.start
|
|||||||
|
|
||||||
export const getBuiltinThemes = () => Promise.all(
|
export const getBuiltinThemes = () => Promise.all(
|
||||||
[
|
[
|
||||||
|
'l-simkey',
|
||||||
|
'l-simkey-noglass',
|
||||||
'l-light',
|
'l-light',
|
||||||
'l-coffee',
|
'l-coffee',
|
||||||
'l-apricot',
|
'l-apricot',
|
||||||
@ -27,6 +29,8 @@ export const getBuiltinThemes = () => Promise.all(
|
|||||||
'l-sushi',
|
'l-sushi',
|
||||||
'l-u0',
|
'l-u0',
|
||||||
|
|
||||||
|
'd-simkey',
|
||||||
|
'd-simkey-noglass',
|
||||||
'd-dark',
|
'd-dark',
|
||||||
'd-persimmon',
|
'd-persimmon',
|
||||||
'd-astro',
|
'd-astro',
|
||||||
|
@ -25,7 +25,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
},
|
},
|
||||||
rememberNoteVisibility: {
|
rememberNoteVisibility: {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
default: false,
|
default: true
|
||||||
},
|
},
|
||||||
defaultNoteVisibility: {
|
defaultNoteVisibility: {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
@ -157,7 +157,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
},
|
},
|
||||||
disableDrawer: {
|
disableDrawer: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: true
|
||||||
},
|
},
|
||||||
useBlurEffectForModal: {
|
useBlurEffectForModal: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
@ -177,7 +177,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
},
|
},
|
||||||
useReactionPickerForContextMenu: {
|
useReactionPickerForContextMenu: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: true
|
||||||
},
|
},
|
||||||
showGapBetweenNotesInTimeline: {
|
showGapBetweenNotesInTimeline: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
@ -197,15 +197,15 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
},
|
},
|
||||||
reactionPickerWidth: {
|
reactionPickerWidth: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 1,
|
default: 3
|
||||||
},
|
},
|
||||||
reactionPickerHeight: {
|
reactionPickerHeight: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 2,
|
default: 4
|
||||||
},
|
},
|
||||||
reactionPickerUseDrawerForMobile: {
|
reactionPickerUseDrawerForMobile: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
recentlyUsedEmojis: {
|
recentlyUsedEmojis: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
@ -269,8 +269,8 @@ type Plugin = {
|
|||||||
/**
|
/**
|
||||||
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
|
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
|
||||||
*/
|
*/
|
||||||
import lightTheme from '@/themes/l-light.json5';
|
import lightTheme from '@/themes/l-simkey.json5';
|
||||||
import darkTheme from '@/themes/d-green-lime.json5';
|
import darkTheme from '@/themes/d-simkey.json5';
|
||||||
|
|
||||||
export class ColdDeviceStorage {
|
export class ColdDeviceStorage {
|
||||||
public static default = {
|
public static default = {
|
||||||
|
@ -250,9 +250,11 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
._panel {
|
._panel {
|
||||||
background: var(--panel);
|
// background: var(--panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
|
|
||||||
._block {
|
._block {
|
||||||
@ -334,6 +336,8 @@ hr {
|
|||||||
background: var(--popup);
|
background: var(--popup);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
contain: content;
|
contain: content;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 廃止
|
// TODO: 廃止
|
||||||
|
78
packages/client/src/themes/d-simkey-noglass.json5
Normal file
78
packages/client/src/themes/d-simkey-noglass.json5
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
id: '971cb22e-c9e6-4a7d-92f9-87cc0ddda561',
|
||||||
|
base: 'dark',
|
||||||
|
name: 'simkey Astro Dark-NoGlass',
|
||||||
|
author: 'sim1222',
|
||||||
|
props: {
|
||||||
|
bg: 'rgba(35, 33, 37, 1)',
|
||||||
|
fg: '#efdab9',
|
||||||
|
cwBg: '#687390',
|
||||||
|
cwFg: '#393f4f',
|
||||||
|
link: '#78b0a0',
|
||||||
|
warn: '#ecb637',
|
||||||
|
badge: '#31b1ce',
|
||||||
|
error: '#ec4137',
|
||||||
|
focus: ':alpha<0.3<@accent',
|
||||||
|
navBg: '@panel',
|
||||||
|
navFg: '@fg',
|
||||||
|
panel: 'rgba(42, 39, 43, 1)',
|
||||||
|
accent: '#81c08b',
|
||||||
|
header: ':alpha<0.7<@bg',
|
||||||
|
infoBg: '#253142',
|
||||||
|
infoFg: '#fff',
|
||||||
|
renote: '#659CC8',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
divider: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
hashtag: '#ff9156',
|
||||||
|
mention: '#ffd152',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
success: '#86b300',
|
||||||
|
buttonBg: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
cwHoverBg: '#707b97',
|
||||||
|
indicator: '@accent',
|
||||||
|
mentionMe: '#fb5d38',
|
||||||
|
messageBg: '@bg',
|
||||||
|
navActive: '@accent',
|
||||||
|
infoWarnBg: '#42321c',
|
||||||
|
infoWarnFg: '#ffbd3e',
|
||||||
|
navHoverFg: ':lighten<17<@fg',
|
||||||
|
dateLabelFg: '@fg',
|
||||||
|
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
panelBorder: '" solid 1px var(--divider)',
|
||||||
|
accentDarken: ':darken<10<@accent',
|
||||||
|
acrylicPanel: ':alpha<0.5<@panel',
|
||||||
|
navIndicator: '@accent',
|
||||||
|
accentLighten: ':lighten<10<@accent',
|
||||||
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
buttonGradateA: '@accent',
|
||||||
|
buttonGradateB: ':hue<-20<@accent',
|
||||||
|
driveFolderBg: ':alpha<0.3<@accent',
|
||||||
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
panelHeaderFg: '@fg',
|
||||||
|
htmlThemeColor: '@bg',
|
||||||
|
panelHighlight: ':lighten<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
|
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
X2: ':darken<2<@panel',
|
||||||
|
X3: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X4: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X5: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X6: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X7: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X8: ':lighten<5<@accent',
|
||||||
|
X9: ':darken<5<@accent',
|
||||||
|
X10: ':alpha<0.4<@accent',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
X12: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X13: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X14: ':alpha<0.5<@navBg',
|
||||||
|
X15: ':alpha<0<@panel',
|
||||||
|
X16: ':alpha<0.7<@panel',
|
||||||
|
},
|
||||||
|
}
|
78
packages/client/src/themes/d-simkey.json5
Normal file
78
packages/client/src/themes/d-simkey.json5
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
id: '1c9f3491-d0d3-4da8-a542-a2d0358d23bc',
|
||||||
|
base: 'dark',
|
||||||
|
name: 'simkey Astro Dark',
|
||||||
|
author: 'sim1222',
|
||||||
|
props: {
|
||||||
|
bg: 'rgba(35, 33, 37, 0.5)',
|
||||||
|
fg: '#efdab9',
|
||||||
|
cwBg: '#687390',
|
||||||
|
cwFg: '#393f4f',
|
||||||
|
link: '#78b0a0',
|
||||||
|
warn: '#ecb637',
|
||||||
|
badge: '#31b1ce',
|
||||||
|
error: '#ec4137',
|
||||||
|
focus: ':alpha<0.3<@accent',
|
||||||
|
navBg: '@panel',
|
||||||
|
navFg: '@fg',
|
||||||
|
panel: 'rgba(42, 39, 43, 0.5)',
|
||||||
|
accent: '#81c08b',
|
||||||
|
header: ':alpha<0.7<@bg',
|
||||||
|
infoBg: '#253142',
|
||||||
|
infoFg: '#fff',
|
||||||
|
renote: '#659CC8',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
divider: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
hashtag: '#ff9156',
|
||||||
|
mention: '#ffd152',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
success: '#86b300',
|
||||||
|
buttonBg: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
cwHoverBg: '#707b97',
|
||||||
|
indicator: '@accent',
|
||||||
|
mentionMe: '#fb5d38',
|
||||||
|
messageBg: '@bg',
|
||||||
|
navActive: '@accent',
|
||||||
|
infoWarnBg: '#42321c',
|
||||||
|
infoWarnFg: '#ffbd3e',
|
||||||
|
navHoverFg: ':lighten<17<@fg',
|
||||||
|
dateLabelFg: '@fg',
|
||||||
|
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
panelBorder: '" solid 1px var(--divider)',
|
||||||
|
accentDarken: ':darken<10<@accent',
|
||||||
|
acrylicPanel: ':alpha<0.5<@panel',
|
||||||
|
navIndicator: '@accent',
|
||||||
|
accentLighten: ':lighten<10<@accent',
|
||||||
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
buttonGradateA: '@accent',
|
||||||
|
buttonGradateB: ':hue<-20<@accent',
|
||||||
|
driveFolderBg: ':alpha<0.3<@accent',
|
||||||
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
panelHeaderFg: '@fg',
|
||||||
|
htmlThemeColor: '@bg',
|
||||||
|
panelHighlight: ':lighten<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
|
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
X2: ':darken<2<@panel',
|
||||||
|
X3: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X4: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X5: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X6: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X7: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
X8: ':lighten<5<@accent',
|
||||||
|
X9: ':darken<5<@accent',
|
||||||
|
X10: ':alpha<0.4<@accent',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
X12: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
X13: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
X14: ':alpha<0.5<@navBg',
|
||||||
|
X15: ':alpha<0<@panel',
|
||||||
|
X16: ':alpha<0.7<@panel',
|
||||||
|
},
|
||||||
|
}
|
50
packages/client/src/themes/l-simkey-noglass.json5
Normal file
50
packages/client/src/themes/l-simkey-noglass.json5
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
id: '2626d839-8579-4b44-80a4-39af123ff74a',
|
||||||
|
name: 'simkey Vivid Light-NoGlass',
|
||||||
|
author: 'sim1222',
|
||||||
|
base: 'light',
|
||||||
|
props: {
|
||||||
|
bg: 'rgba(250, 250, 250, 1)',
|
||||||
|
fg: '#444',
|
||||||
|
cwBg: '#b1b9c1',
|
||||||
|
cwFg: '#fff',
|
||||||
|
link: '#ff9400',
|
||||||
|
panel: 'rgba(255, 255, 255, 1)',
|
||||||
|
accent: '#81c08b',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
infoBg: '#e5f5ff',
|
||||||
|
infoFg: '#72818a',
|
||||||
|
renote: '#659CC8',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
divider: 'rgba(0, 0, 0, 0.08)',
|
||||||
|
hashtag: '#92d400',
|
||||||
|
mention: '@accent',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
buttonBg: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
cwHoverBg: '#bbc4ce',
|
||||||
|
mentionMe: '@mention',
|
||||||
|
infoWarnBg: '#fff0db',
|
||||||
|
infoWarnFg: '#8f6e31',
|
||||||
|
navHoverFg: ':darken<17<@fg',
|
||||||
|
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
fgHighlighted: ':darken<3<@fg',
|
||||||
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
|
panelHighlight: ':darken<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||||
|
panelHeaderDivider: '@divider',
|
||||||
|
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
|
||||||
|
X3: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
X4: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
X5: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
X6: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
X7: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
X12: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
X13: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
},
|
||||||
|
}
|
50
packages/client/src/themes/l-simkey.json5
Normal file
50
packages/client/src/themes/l-simkey.json5
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
id: '80b5d38c-c281-403b-9fc5-3bb4f1dc19b8',
|
||||||
|
name: 'simkey Vivid Light',
|
||||||
|
author: 'sim1222',
|
||||||
|
base: 'light',
|
||||||
|
props: {
|
||||||
|
bg: 'rgba(250, 250, 250, 0.7)',
|
||||||
|
fg: '#444',
|
||||||
|
cwBg: '#b1b9c1',
|
||||||
|
cwFg: '#fff',
|
||||||
|
link: '#ff9400',
|
||||||
|
panel: 'rgba(255, 255, 255, 0.6)',
|
||||||
|
accent: '#81c08b',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
infoBg: '#e5f5ff',
|
||||||
|
infoFg: '#72818a',
|
||||||
|
renote: '#659CC8',
|
||||||
|
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
divider: 'rgba(0, 0, 0, 0.08)',
|
||||||
|
hashtag: '#92d400',
|
||||||
|
mention: '@accent',
|
||||||
|
modalBg: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
buttonBg: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
cwHoverBg: '#bbc4ce',
|
||||||
|
mentionMe: '@mention',
|
||||||
|
infoWarnBg: '#fff0db',
|
||||||
|
infoWarnFg: '#8f6e31',
|
||||||
|
navHoverFg: ':darken<17<@fg',
|
||||||
|
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
fgHighlighted: ':darken<3<@fg',
|
||||||
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
|
panelHighlight: ':darken<3<@panel',
|
||||||
|
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
|
||||||
|
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||||
|
panelHeaderDivider: '@divider',
|
||||||
|
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
|
||||||
|
X3: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
X4: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
X5: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
X6: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
X7: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
X11: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
X12: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
X13: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
},
|
||||||
|
}
|
123
packages/client/src/ui/_common_old/common.vue
Normal file
123
packages/client/src/ui/_common_old/common.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="popup.component"
|
||||||
|
v-for="popup in popups"
|
||||||
|
:key="popup.id"
|
||||||
|
v-bind="popup.props"
|
||||||
|
v-on="popup.events"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<XUpload v-if="uploads.length > 0"/>
|
||||||
|
|
||||||
|
<XStreamIndicator/>
|
||||||
|
|
||||||
|
<div v-if="pendingApiRequestsCount > 0" id="wait"></div>
|
||||||
|
|
||||||
|
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
|
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')),
|
||||||
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const onNotification = notification => {
|
||||||
|
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
||||||
|
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
stream.send('readNotification', {
|
||||||
|
id: notification.id
|
||||||
|
});
|
||||||
|
|
||||||
|
popup(defineAsyncComponent(() => import('@/components/MkNotificationToast.vue')), {
|
||||||
|
notification
|
||||||
|
}, {}, 'closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
sound.play('notification');
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($i) {
|
||||||
|
const connection = stream.useChannel('main', null, 'UI');
|
||||||
|
connection.on('notification', onNotification);
|
||||||
|
|
||||||
|
//#region Listen message from SW
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
swInject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploads,
|
||||||
|
popups,
|
||||||
|
pendingApiRequestsCount,
|
||||||
|
dev: _DEV_,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@keyframes dev-ticker-blink {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#wait {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 4000000;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: progress-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#devTicker {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2147483647;
|
||||||
|
color: #ff0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 4px 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
animation: dev-ticker-blink 2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
286
packages/client/src/ui/_common_old/sidebar-for-mobile.vue
Normal file
286
packages/client/src/ui/_common_old/sidebar-for-mobile.vue
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<div class="kmwsukvl">
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
|
||||||
|
<button v-click-anime class="item _button instance" @click="openInstanceMenu">
|
||||||
|
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="middle">
|
||||||
|
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
|
||||||
|
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
||||||
|
</MkA>
|
||||||
|
<template v-for="item in menu">
|
||||||
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
|
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: navbarItemDef[item].active }]" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||||
|
<i class="icon fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
|
||||||
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
|
||||||
|
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
|
||||||
|
</MkA>
|
||||||
|
<button v-click-anime class="item _button" @click="more">
|
||||||
|
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
||||||
|
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</button>
|
||||||
|
<MkA v-click-anime class="item" active-class="active" to="/settings">
|
||||||
|
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<button class="item _button post" data-cy-open-post-form @click="os.post">
|
||||||
|
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
||||||
|
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, defineComponent, ref, toRef, watch } from 'vue';
|
||||||
|
import { host } from '@/config';
|
||||||
|
import { search } from '@/scripts/search';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const menu = toRef(defaultStore.state, 'menu');
|
||||||
|
const otherMenuItemIndicated = computed(() => {
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (menu.value.includes(def)) continue;
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAccountMenu(ev: MouseEvent) {
|
||||||
|
openAccountMenu_({
|
||||||
|
withExtraOperation: true,
|
||||||
|
}, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInstanceMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: instance.name ?? host,
|
||||||
|
type: 'label',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.instanceInfo,
|
||||||
|
icon: 'fas fa-info-circle',
|
||||||
|
to: '/about',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.customEmojis,
|
||||||
|
icon: 'fas fa-laugh',
|
||||||
|
to: '/about#emojis',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.federation,
|
||||||
|
icon: 'fas fa-globe',
|
||||||
|
to: '/about#federation',
|
||||||
|
}], ev.currentTarget ?? ev.target, {
|
||||||
|
align: 'left',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function more() {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, {
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.kmwsukvl {
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
> .body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 38px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 30px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 16px 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding-left: 24px;
|
||||||
|
line-height: 2.85rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--navFg);
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--navHoverFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--navActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
527
packages/client/src/ui/_common_old/sidebar.vue
Normal file
527
packages/client/src/ui/_common_old/sidebar.vue
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mvcprjjd" :class="{ iconOnly }">
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
|
||||||
|
<button v-click-anime v-tooltip.noDelay.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
|
||||||
|
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="middle">
|
||||||
|
<MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact>
|
||||||
|
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
|
||||||
|
</MkA>
|
||||||
|
<template v-for="item in menu">
|
||||||
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
|
<component
|
||||||
|
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
|
||||||
|
v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
|
||||||
|
v-click-anime
|
||||||
|
v-tooltip.noDelay.right="i18n.ts[navbarItemDef[item].title]"
|
||||||
|
class="item _button"
|
||||||
|
:class="[item, { active: navbarItemDef[item].active }]"
|
||||||
|
active-class="active"
|
||||||
|
:to="navbarItemDef[item].to"
|
||||||
|
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
|
||||||
|
>
|
||||||
|
<i class="icon fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ i18n.ts[navbarItemDef[item].title] }}</span>
|
||||||
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.noDelay.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin">
|
||||||
|
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||||
|
</MkA>
|
||||||
|
<button v-click-anime class="item _button" @click="more">
|
||||||
|
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ i18n.ts.more }}</span>
|
||||||
|
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
|
||||||
|
</button>
|
||||||
|
<MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.settings" class="item" active-class="active" to="/settings">
|
||||||
|
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<button v-tooltip.noDelay.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post">
|
||||||
|
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="item _button account" @click="openAccountMenu">
|
||||||
|
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { host } from '@/config';
|
||||||
|
|
||||||
|
const iconOnly = ref(false);
|
||||||
|
|
||||||
|
const menu = computed(() => defaultStore.state.menu);
|
||||||
|
const otherMenuItemIndicated = computed(() => {
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (menu.value.includes(def)) continue;
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calcViewState = () => {
|
||||||
|
iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
|
||||||
|
};
|
||||||
|
|
||||||
|
calcViewState();
|
||||||
|
|
||||||
|
window.addEventListener('resize', calcViewState);
|
||||||
|
|
||||||
|
watch(defaultStore.reactiveState.menuDisplay, () => {
|
||||||
|
calcViewState();
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAccountMenu(ev: MouseEvent) {
|
||||||
|
openAccountMenu_({
|
||||||
|
withExtraOperation: true,
|
||||||
|
}, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInstanceMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: instance.name ?? host,
|
||||||
|
type: 'label',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.instanceInfo,
|
||||||
|
icon: 'fas fa-info-circle',
|
||||||
|
to: '/about',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.customEmojis,
|
||||||
|
icon: 'fas fa-laugh',
|
||||||
|
to: '/about#emojis',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.federation,
|
||||||
|
icon: 'fas fa-globe',
|
||||||
|
to: '/about#federation',
|
||||||
|
}, 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function more(ev: MouseEvent) {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
|
||||||
|
src: ev.currentTarget ?? ev.target,
|
||||||
|
}, {
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mvcprjjd {
|
||||||
|
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||||
|
$nav-width: 250px;
|
||||||
|
$nav-icon-only-width: 86px;
|
||||||
|
$avatar-size: 32px;
|
||||||
|
$avatar-margin: 8px;
|
||||||
|
|
||||||
|
flex: 0 0 $nav-width;
|
||||||
|
width: $nav-width;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: clip;
|
||||||
|
background: var(--navBg);
|
||||||
|
contain: strict;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.iconOnly) {
|
||||||
|
> .body {
|
||||||
|
width: $nav-width;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 38px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 30px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 16px 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding-left: 30px;
|
||||||
|
line-height: 2.85rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--navFg);
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--navHoverFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--navActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 34px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.iconOnly {
|
||||||
|
flex: 0 0 $nav-icon-only-width;
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 52px;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 8px auto;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding: 18px 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 24px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon, > .text {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
60
packages/client/src/ui/_common_old/stream-indicator.vue
Normal file
60
packages/client/src/ui/_common_old/stream-indicator.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" class="nsbbhtug" @click="resetDisconnected">
|
||||||
|
<div>{{ $ts.disconnectedFromServer }}</div>
|
||||||
|
<div class="command">
|
||||||
|
<button class="_textButton" @click="reload">{{ $ts.reload }}</button>
|
||||||
|
<button class="_textButton">{{ $ts.doNothing }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onUnmounted } from 'vue';
|
||||||
|
import { stream } from '@/stream';
|
||||||
|
|
||||||
|
let hasDisconnected = $ref(false);
|
||||||
|
|
||||||
|
function onDisconnected() {
|
||||||
|
hasDisconnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDisconnected() {
|
||||||
|
hasDisconnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on('_disconnected_', onDisconnected);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stream.off('_disconnected_', onDisconnected);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.nsbbhtug {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 16385;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
opacity: 0.8;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 320px;
|
||||||
|
|
||||||
|
> .command {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
padding: 0.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
35
packages/client/src/ui/_common_old/sw-inject.ts
Normal file
35
packages/client/src/ui/_common_old/sw-inject.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { inject } from 'vue';
|
||||||
|
import { post } from '@/os';
|
||||||
|
import { $i, login } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
|
import { mainRouter } from '@/router';
|
||||||
|
|
||||||
|
export function swInject() {
|
||||||
|
navigator.serviceWorker.addEventListener('message', ev => {
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log('sw msg', ev.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.data.type !== 'order') return;
|
||||||
|
|
||||||
|
if (ev.data.loginId !== $i?.id) {
|
||||||
|
return getAccountFromId(ev.data.loginId).then(account => {
|
||||||
|
if (!account) return;
|
||||||
|
return login(account.token, ev.data.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ev.data.order) {
|
||||||
|
case 'post':
|
||||||
|
return post(ev.data.options);
|
||||||
|
case 'push':
|
||||||
|
if (mainRouter.currentRoute.value.path === ev.data.url) {
|
||||||
|
return window.scroll({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
return mainRouter.push(ev.data.url);
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
128
packages/client/src/ui/_common_old/upload.vue
Normal file
128
packages/client/src/ui/_common_old/upload.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mk-uploader _acrylic" :style="{ zIndex }">
|
||||||
|
<ol v-if="uploads.length > 0">
|
||||||
|
<li v-for="ctx in uploads" :key="ctx.id">
|
||||||
|
<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
|
||||||
|
<div class="top">
|
||||||
|
<p class="name"><i class="fas fa-spinner fa-pulse"></i>{{ ctx.name }}</p>
|
||||||
|
<p class="status">
|
||||||
|
<span v-if="ctx.progressValue === undefined" class="initing">{{ $ts.waiting }}<MkEllipsis/></span>
|
||||||
|
<span v-if="ctx.progressValue !== undefined" class="kb">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
|
||||||
|
<span v-if="ctx.progressValue !== undefined" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { uploads } from '@/scripts/upload';
|
||||||
|
|
||||||
|
const zIndex = os.claimZIndex('high');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mk-uploader {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
width: 260px;
|
||||||
|
top: 32px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.mk-uploader:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li {
|
||||||
|
display: grid;
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
border-top: solid 8px transparent;
|
||||||
|
grid-template-columns: 36px calc(100% - 44px);
|
||||||
|
grid-template-rows: 1fr 8px;
|
||||||
|
column-gap: 8px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li:first-child {
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .img {
|
||||||
|
display: block;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
grid-column: 1/2;
|
||||||
|
grid-row: 1/3;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top {
|
||||||
|
display: flex;
|
||||||
|
grid-column: 2/3;
|
||||||
|
grid-row: 1/2;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .name {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px 0 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .name > i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .initing {
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .kb {
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .percentage {
|
||||||
|
display: inline-block;
|
||||||
|
width: 48px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .percentage:after {
|
||||||
|
content: '%';
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > progress {
|
||||||
|
display: block;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
grid-column: 2/3;
|
||||||
|
grid-row: 2/3;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > progress::-webkit-progress-value {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > progress::-webkit-progress-bar {
|
||||||
|
//background: var(--accentAlpha01);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
305
packages/client/src/ui/deckold.vue
Normal file
305
packages/client/src/ui/deckold.vue
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': '12' + 'px' }"
|
||||||
|
@contextmenu.self.prevent="onContextmenu"
|
||||||
|
>
|
||||||
|
<XSidebar v-if="!isMobile"/>
|
||||||
|
|
||||||
|
<template v-for="ids in layout">
|
||||||
|
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||||
|
<section
|
||||||
|
v-if="ids.length > 1"
|
||||||
|
class="folder column"
|
||||||
|
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
|
||||||
|
>
|
||||||
|
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
|
||||||
|
</section>
|
||||||
|
<DeckColumnCore
|
||||||
|
v-else
|
||||||
|
:ref="ids[0]"
|
||||||
|
:key="ids[0]"
|
||||||
|
class="column"
|
||||||
|
:column="columns.find(c => c.id === ids[0])"
|
||||||
|
:is-stacked="false"
|
||||||
|
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
|
||||||
|
@parent-focus="moveFocus(ids[0], $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="isMobile" class="buttons">
|
||||||
|
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||||
|
<button class="button home _button" @click="mainRouter.push('/')"><i class="fas fa-home"></i></button>
|
||||||
|
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||||
|
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition :name="$store.state.animation ? 'menu-back' : ''">
|
||||||
|
<div
|
||||||
|
v-if="drawerMenuShowing"
|
||||||
|
class="menu-back _modalBg"
|
||||||
|
@click="drawerMenuShowing = false"
|
||||||
|
@touchstart.passive="drawerMenuShowing = false"
|
||||||
|
></div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<transition :name="$store.state.animation ? 'menu' : ''">
|
||||||
|
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<XCommon/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import XCommon from './_common_old/common.vue';
|
||||||
|
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deckold/deck-store';
|
||||||
|
import DeckColumnCore from '@/ui/deckold/column-core.vue';
|
||||||
|
import XSidebar from '@/ui/_common_old/sidebar.vue';
|
||||||
|
import XDrawerMenu from '@/ui/_common_old/sidebar-for-mobile.vue';
|
||||||
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
import { getScrollContainer } from '@/scripts/scroll';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { mainRouter } from '@/router';
|
||||||
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
|
|
||||||
|
mainRouter.navHook = (path): boolean => {
|
||||||
|
const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
|
||||||
|
if (deckStore.state.navWindow || noMainColumn) {
|
||||||
|
os.pageWindow(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMobile = ref(window.innerWidth <= 500);
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
isMobile.value = window.innerWidth <= 500;
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawerMenuShowing = ref(false);
|
||||||
|
|
||||||
|
const route = 'TODO';
|
||||||
|
watch(route, () => {
|
||||||
|
drawerMenuShowing.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = deckStore.reactiveState.columns;
|
||||||
|
const layout = deckStore.reactiveState.layout;
|
||||||
|
const menuIndicated = computed(() => {
|
||||||
|
if ($i == null) return false;
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addColumn = async (ev) => {
|
||||||
|
const columns = [
|
||||||
|
'main',
|
||||||
|
'widgets',
|
||||||
|
'notifications',
|
||||||
|
'tl',
|
||||||
|
'antenna',
|
||||||
|
'list',
|
||||||
|
'mentions',
|
||||||
|
'direct',
|
||||||
|
];
|
||||||
|
|
||||||
|
const { canceled, result: column } = await os.select({
|
||||||
|
title: i18n.ts._deck.addColumn,
|
||||||
|
items: columns.map(column => ({
|
||||||
|
value: column, text: i18n.t('_deck._columns.' + column)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
addColumnToStore({
|
||||||
|
type: column,
|
||||||
|
id: uuid(),
|
||||||
|
name: i18n.t('_deck._columns.' + column),
|
||||||
|
width: 330,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContextmenu = (ev) => {
|
||||||
|
os.contextMenu([{
|
||||||
|
text: i18n.ts._deck.addColumn,
|
||||||
|
action: addColumn,
|
||||||
|
}], ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
provide('shouldSpacerMin', true);
|
||||||
|
if (deckStore.state.navWindow) {
|
||||||
|
provide('navHook', (url) => {
|
||||||
|
os.pageWindow(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.documentElement.style.scrollBehavior = 'auto';
|
||||||
|
window.addEventListener('wheel', (ev) => {
|
||||||
|
if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
|
||||||
|
document.documentElement.scrollLeft += ev.deltaY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadDeck();
|
||||||
|
|
||||||
|
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
||||||
|
// TODO??
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.menu-enter-active,
|
||||||
|
.menu-leave-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
.menu-enter-from,
|
||||||
|
.menu-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-back-enter-active,
|
||||||
|
.menu-back-leave-active {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
.menu-back-enter-from,
|
||||||
|
.menu-back-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mk-deck {
|
||||||
|
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||||
|
|
||||||
|
// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
|
||||||
|
--margin: var(--marginHalf);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--deckMargin);
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
> .column:first-of-type {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .column:last-of-type {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isMobile {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .column {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: var(--deckMargin);
|
||||||
|
|
||||||
|
&.folder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-bottom: var(--deckMargin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .buttons {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
margin: auto;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--fg);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
height: 60px;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--X2);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: var(--indicator);
|
||||||
|
font-size: 16px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu-back {
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
|
width: 240px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
63
packages/client/src/ui/deckold/antenna-column.vue
Normal file
63
packages/client/src/ui/deckold/antenna-column.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :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>
|
||||||
|
|
||||||
|
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'loaded'): void;
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.column.antennaId == null) {
|
||||||
|
setAntenna();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setAntenna() {
|
||||||
|
const antennas = await os.api('antennas/list');
|
||||||
|
const { canceled, result: antenna } = await os.select({
|
||||||
|
title: i18n.ts.selectAntenna,
|
||||||
|
items: antennas.map(x => ({
|
||||||
|
value: x, text: x.name
|
||||||
|
})),
|
||||||
|
default: props.column.antennaId
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
antennaId: antenna.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
function focus() {
|
||||||
|
timeline.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
44
packages/client/src/ui/deckold/column-core.vue
Normal file
44
packages/client/src/ui/deckold/column-core.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<!-- TODO: リファクタの余地がありそう -->
|
||||||
|
<div v-if="!column">たぶん見えちゃいけないやつ</div>
|
||||||
|
<XMainColumn v-else-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XMainColumn from './main-column.vue';
|
||||||
|
import XTlColumn from './tl-column.vue';
|
||||||
|
import XAntennaColumn from './antenna-column.vue';
|
||||||
|
import XListColumn from './list-column.vue';
|
||||||
|
import XNotificationsColumn from './notifications-column.vue';
|
||||||
|
import XWidgetsColumn from './widgets-column.vue';
|
||||||
|
import XMentionsColumn from './mentions-column.vue';
|
||||||
|
import XDirectColumn from './direct-column.vue';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column?: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
export default defineComponent({
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$children[0].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
393
packages/client/src/ui/deckold/column.vue
Normal file
393
packages/client/src/ui/deckold/column.vue
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
<template>
|
||||||
|
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||||
|
<section v-hotkey="keymap" class="dnpfarvg _panel _narrow_"
|
||||||
|
:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
|
||||||
|
:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
|
||||||
|
@dragover.prevent.stop="onDragover"
|
||||||
|
@dragleave="onDragleave"
|
||||||
|
@drop.prevent.stop="onDrop"
|
||||||
|
>
|
||||||
|
<header :class="{ indicated }"
|
||||||
|
draggable="true"
|
||||||
|
@click="goTop"
|
||||||
|
@dragstart="onDragstart"
|
||||||
|
@dragend="onDragend"
|
||||||
|
@contextmenu.prevent.stop="onContextmenu"
|
||||||
|
>
|
||||||
|
<button v-if="isStacked && !isMainColumn" class="toggleActive _button" @click="toggleActive">
|
||||||
|
<template v-if="active"><i class="fas fa-angle-up"></i></template>
|
||||||
|
<template v-else><i class="fas fa-angle-down"></i></template>
|
||||||
|
</button>
|
||||||
|
<div class="action">
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</div>
|
||||||
|
<span class="header"><slot name="header"></slot></span>
|
||||||
|
<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-ellipsis"></i></button>
|
||||||
|
</header>
|
||||||
|
<div v-show="active" ref="body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</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 { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
provide('shouldHeaderThin', true);
|
||||||
|
provide('shouldOmitHeaderTitle', true);
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked?: boolean;
|
||||||
|
func?: DeckFunc | null;
|
||||||
|
naked?: boolean;
|
||||||
|
indicated?: boolean;
|
||||||
|
}>(), {
|
||||||
|
isStacked: false,
|
||||||
|
func: null,
|
||||||
|
naked: false,
|
||||||
|
indicated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
(ev: 'change-active-state', v: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let body = $ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
let dragging = $ref(false);
|
||||||
|
watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
|
||||||
|
|
||||||
|
let draghover = $ref(false);
|
||||||
|
let dropready = $ref(false);
|
||||||
|
|
||||||
|
const isMainColumn = $computed(() => props.column.type === 'main');
|
||||||
|
const active = $computed(() => props.column.active !== false);
|
||||||
|
watch($$(active), v => emit('change-active-state', v));
|
||||||
|
|
||||||
|
const keymap = $computed(() => ({
|
||||||
|
'shift+up': () => emit('parent-focus', 'up'),
|
||||||
|
'shift+down': () => emit('parent-focus', 'down'),
|
||||||
|
'shift+left': () => emit('parent-focus', 'left'),
|
||||||
|
'shift+right': () => emit('parent-focus', 'right'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
os.deckGlobalEvents.on('column.dragStart', onOtherDragStart);
|
||||||
|
os.deckGlobalEvents.on('column.dragEnd', onOtherDragEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
os.deckGlobalEvents.off('column.dragStart', onOtherDragStart);
|
||||||
|
os.deckGlobalEvents.off('column.dragEnd', onOtherDragEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onOtherDragStart() {
|
||||||
|
dropready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOtherDragEnd() {
|
||||||
|
dropready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleActive() {
|
||||||
|
if (!props.isStacked) return;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
active: !props.column.active,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMenu() {
|
||||||
|
const items = [{
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
text: i18n.ts.edit,
|
||||||
|
action: async () => {
|
||||||
|
const { canceled, result } = await os.form(props.column.name, {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
label: i18n.ts.name,
|
||||||
|
default: props.column.name,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: 'number',
|
||||||
|
label: i18n.ts.width,
|
||||||
|
default: props.column.width,
|
||||||
|
},
|
||||||
|
flexible: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: i18n.ts.flexible,
|
||||||
|
default: props.column.flexible,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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, {
|
||||||
|
icon: 'fas fa-window-restore',
|
||||||
|
text: i18n.ts._deck.stackLeft,
|
||||||
|
action: () => {
|
||||||
|
stackLeftColumn(props.column.id);
|
||||||
|
},
|
||||||
|
}, props.isStacked ? {
|
||||||
|
icon: 'fas fa-window-maximize',
|
||||||
|
text: i18n.ts._deck.popRight,
|
||||||
|
action: () => {
|
||||||
|
popRightColumn(props.column.id);
|
||||||
|
},
|
||||||
|
} : undefined, null, {
|
||||||
|
icon: 'fas fa-trash-alt',
|
||||||
|
text: i18n.ts.remove,
|
||||||
|
danger: true,
|
||||||
|
action: () => {
|
||||||
|
removeColumn(props.column.id);
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (props.func) {
|
||||||
|
items.unshift(null);
|
||||||
|
items.unshift({
|
||||||
|
icon: props.func.icon,
|
||||||
|
text: props.func.title,
|
||||||
|
action: props.func.handler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSettingsMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu(getMenu(), ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContextmenu(ev: MouseEvent) {
|
||||||
|
os.contextMenu(getMenu(), ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goTop() {
|
||||||
|
body.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragstart(ev) {
|
||||||
|
ev.dataTransfer.effectAllowed = 'move';
|
||||||
|
ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
|
||||||
|
|
||||||
|
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
|
||||||
|
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
|
||||||
|
window.setTimeout(() => {
|
||||||
|
dragging = true;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragend(ev) {
|
||||||
|
dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragover(ev) {
|
||||||
|
// 自分自身がドラッグされている場合
|
||||||
|
if (dragging) {
|
||||||
|
// 自分自身にはドロップさせない
|
||||||
|
ev.dataTransfer.dropEffect = 'none';
|
||||||
|
} else {
|
||||||
|
const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_;
|
||||||
|
|
||||||
|
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||||
|
|
||||||
|
if (isDeckColumn) draghover = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragleave() {
|
||||||
|
draghover = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(ev) {
|
||||||
|
draghover = false;
|
||||||
|
os.deckGlobalEvents.emit('column.dragEnd');
|
||||||
|
|
||||||
|
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
|
||||||
|
if (id != null && id !== '') {
|
||||||
|
swapColumn(props.column.id, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dnpfarvg {
|
||||||
|
--root-margin: 10px;
|
||||||
|
--deckColumnHeaderHeight: 42px;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: content;
|
||||||
|
box-shadow: 0 0 8px 0 var(--shadow);
|
||||||
|
|
||||||
|
&.draghover {
|
||||||
|
box-shadow: 0 0 0 2px var(--focus);
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
box-shadow: 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dropready {
|
||||||
|
* {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
flex-basis: var(--deckColumnHeaderHeight);
|
||||||
|
min-height: var(--deckColumnHeaderHeight);
|
||||||
|
|
||||||
|
> header.indicated {
|
||||||
|
box-shadow: 4px 0px var(--accent) inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.naked {
|
||||||
|
background: var(--acrylicBg) !important;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(10px));
|
||||||
|
backdrop-filter: var(--blur, blur(10px));
|
||||||
|
|
||||||
|
> header {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.paged {
|
||||||
|
background: var(--bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
|
line-height: var(--deckColumnHeaderHeight);
|
||||||
|
height: var(--deckColumnHeaderHeight);
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--panelHeaderFg);
|
||||||
|
background: var(--panelHeaderBg);
|
||||||
|
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&, * {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indicated {
|
||||||
|
box-shadow: 0 3px 0 0 var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .header {
|
||||||
|
display: inline-block;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span:only-of-type {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .toggleActive,
|
||||||
|
> .action > ::v-deep(*),
|
||||||
|
> .menu {
|
||||||
|
z-index: 1;
|
||||||
|
width: var(--deckColumnHeaderHeight);
|
||||||
|
line-height: var(--deckColumnHeaderHeight);
|
||||||
|
color: var(--faceTextButton);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--faceTextButtonHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: var(--faceTextButtonActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .toggleActive, > .action {
|
||||||
|
margin-left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .action {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .action:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: calc(100% - var(--deckColumnHeaderHeight));
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden; // Safari does not supports clip
|
||||||
|
overflow-x: clip;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
317
packages/client/src/ui/deckold/deck-store.ts
Normal file
317
packages/client/src/ui/deckold/deck-store.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { throttle } from 'throttle-debounce';
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
import { notificationTypes } from 'misskey-js';
|
||||||
|
import { Storage } from '../../pizzax';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { api } from '@/os';
|
||||||
|
|
||||||
|
type ColumnWidget = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Column = {
|
||||||
|
id: string;
|
||||||
|
type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'list' | 'mentions' | 'direct';
|
||||||
|
name: string | null;
|
||||||
|
width: number;
|
||||||
|
widgets?: ColumnWidget[];
|
||||||
|
active?: boolean;
|
||||||
|
flexible?: boolean;
|
||||||
|
antennaId?: string;
|
||||||
|
listId?: string;
|
||||||
|
includingTypes?: typeof notificationTypes[number][];
|
||||||
|
tl?: 'home' | 'local' | 'social' | 'global';
|
||||||
|
};
|
||||||
|
|
||||||
|
function copy<T>(x: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deckStore = markRaw(new Storage('deck', {
|
||||||
|
profile: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: [] as Column[],
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: [] as Column['id'][][],
|
||||||
|
},
|
||||||
|
columnAlign: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: 'left' as 'left' | 'right' | 'center',
|
||||||
|
},
|
||||||
|
alwaysShowMainColumn: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
navWindow: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
columnMargin: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: 12,
|
||||||
|
},
|
||||||
|
columnHeaderHeight: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: 42,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const loadDeck = async () => {
|
||||||
|
let deck;
|
||||||
|
|
||||||
|
try {
|
||||||
|
deck = await api('i/registry/get', {
|
||||||
|
scope: ['client', 'deck', 'profiles'],
|
||||||
|
key: deckStore.state.profile,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'NO_SUCH_KEY') {
|
||||||
|
// 後方互換性のため
|
||||||
|
if (deckStore.state.profile === 'default') {
|
||||||
|
saveDeck();
|
||||||
|
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']]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
deckStore.set('columns', deck.columns);
|
||||||
|
deckStore.set('layout', deck.layout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
|
||||||
|
export const saveDeck = throttle(1000, () => {
|
||||||
|
api('i/registry/set', {
|
||||||
|
scope: ['client', 'deck', 'profiles'],
|
||||||
|
key: deckStore.state.profile,
|
||||||
|
value: {
|
||||||
|
columns: deckStore.reactiveState.columns.value,
|
||||||
|
layout: deckStore.reactiveState.layout.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
deckStore.push('layout', [column.id]);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeColumn(id: Column['id']) {
|
||||||
|
deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
|
||||||
|
deckStore.set('layout', deckStore.state.layout
|
||||||
|
.map(ids => ids.filter(_id => _id !== id))
|
||||||
|
.filter(ids => ids.length > 0));
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||||
|
const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1);
|
||||||
|
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
|
||||||
|
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
||||||
|
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
|
||||||
|
const layout = copy(deckStore.state.layout);
|
||||||
|
layout[aX][aY] = b;
|
||||||
|
layout[bX][bY] = a;
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapLeftColumn(id: Column['id']) {
|
||||||
|
const layout = copy(deckStore.state.layout);
|
||||||
|
deckStore.state.layout.some((ids, i) => {
|
||||||
|
if (ids.includes(id)) {
|
||||||
|
const left = deckStore.state.layout[i - 1];
|
||||||
|
if (left) {
|
||||||
|
layout[i - 1] = deckStore.state.layout[i];
|
||||||
|
layout[i] = left;
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapRightColumn(id: Column['id']) {
|
||||||
|
const layout = copy(deckStore.state.layout);
|
||||||
|
deckStore.state.layout.some((ids, i) => {
|
||||||
|
if (ids.includes(id)) {
|
||||||
|
const right = deckStore.state.layout[i + 1];
|
||||||
|
if (right) {
|
||||||
|
layout[i + 1] = deckStore.state.layout[i];
|
||||||
|
layout[i] = right;
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapUpColumn(id: Column['id']) {
|
||||||
|
const layout = copy(deckStore.state.layout);
|
||||||
|
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
|
const ids = copy(deckStore.state.layout[idsIndex]);
|
||||||
|
ids.some((x, i) => {
|
||||||
|
if (x === id) {
|
||||||
|
const up = ids[i - 1];
|
||||||
|
if (up) {
|
||||||
|
ids[i - 1] = id;
|
||||||
|
ids[i] = up;
|
||||||
|
|
||||||
|
layout[idsIndex] = ids;
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapDownColumn(id: Column['id']) {
|
||||||
|
const layout = copy(deckStore.state.layout);
|
||||||
|
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
|
const ids = copy(deckStore.state.layout[idsIndex]);
|
||||||
|
ids.some((x, i) => {
|
||||||
|
if (x === id) {
|
||||||
|
const down = ids[i + 1];
|
||||||
|
if (down) {
|
||||||
|
ids[i + 1] = id;
|
||||||
|
ids[i] = down;
|
||||||
|
|
||||||
|
layout[idsIndex] = ids;
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stackLeftColumn(id: Column['id']) {
|
||||||
|
let layout = copy(deckStore.state.layout);
|
||||||
|
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
|
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||||
|
layout[i - 1].push(id);
|
||||||
|
layout = layout.filter(ids => ids.length > 0);
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function popRightColumn(id: Column['id']) {
|
||||||
|
let layout = copy(deckStore.state.layout);
|
||||||
|
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
|
const affected = layout[i];
|
||||||
|
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||||
|
layout.splice(i + 1, 0, [id]);
|
||||||
|
layout = layout.filter(ids => ids.length > 0);
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
|
||||||
|
const columns = copy(deckStore.state.columns);
|
||||||
|
for (const column of columns) {
|
||||||
|
if (affected.includes(column.id)) {
|
||||||
|
column.active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||||
|
const columns = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (column == null) return;
|
||||||
|
if (column.widgets == null) column.widgets = [];
|
||||||
|
column.widgets.unshift(widget);
|
||||||
|
columns[columnIndex] = column;
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||||
|
const columns = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (column == null) return;
|
||||||
|
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||||
|
columns[columnIndex] = column;
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||||
|
const columns = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (column == null) return;
|
||||||
|
column.widgets = widgets;
|
||||||
|
columns[columnIndex] = column;
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
|
||||||
|
const columns = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (column == null) return;
|
||||||
|
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||||
|
...w,
|
||||||
|
data: widgetData,
|
||||||
|
} : w);
|
||||||
|
columns[columnIndex] = column;
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||||
|
const columns = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const currentColumn = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (currentColumn == null) return;
|
||||||
|
for (const [k, v] of Object.entries(column)) {
|
||||||
|
currentColumn[k] = v;
|
||||||
|
}
|
||||||
|
columns[columnIndex] = currentColumn;
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
saveDeck();
|
||||||
|
}
|
31
packages/client/src/ui/deckold/direct-column.vue
Normal file
31
packages/client/src/ui/deckold/direct-column.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
|
<XNotes :pagination="pagination"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XNotes from '@/components/MkNotes.vue';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
endpoint: 'notes/mentions' as const,
|
||||||
|
limit: 10,
|
||||||
|
params: {
|
||||||
|
visibility: 'specified'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
66
packages/client/src/ui/deckold/list-column.vue
Normal file
66
packages/client/src/ui/deckold/list-column.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :func="{ handler: setList, title: $ts.selectList }" :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>
|
||||||
|
|
||||||
|
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'loaded'): void;
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||||
|
|
||||||
|
if (props.column.listId == null) {
|
||||||
|
setList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setList() {
|
||||||
|
const lists = await os.api('users/lists/list');
|
||||||
|
const { canceled, result: list } = await os.select({
|
||||||
|
title: i18n.ts.selectList,
|
||||||
|
items: lists.map(x => ({
|
||||||
|
value: x, text: x.name
|
||||||
|
})),
|
||||||
|
default: props.column.listId
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
listId: list.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
function focus() {
|
||||||
|
timeline.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
watch: {
|
||||||
|
mediaOnly() {
|
||||||
|
(this.$refs.timeline as any).reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
68
packages/client/src/ui/deckold/main-column.vue
Normal file
68
packages/client/src/ui/deckold/main-column.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header>
|
||||||
|
<template v-if="pageMetadata?.value">
|
||||||
|
<i :class="pageMetadata?.value.icon"></i>
|
||||||
|
{{ pageMetadata?.value.title }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<RouterView @contextmenu.stop="onContextmenu"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ComputedRef, provide } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import { deckStore, Column } from '@/ui/deck/deck-store';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { mainRouter } from '@/router';
|
||||||
|
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||||
|
|
||||||
|
provide('router', mainRouter);
|
||||||
|
provideMetadataReceiver((info) => {
|
||||||
|
pageMetadata = info;
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
function back() {
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function onContextmenu(ev: MouseEvent) {
|
||||||
|
if (!ev.target) return;
|
||||||
|
|
||||||
|
const isLink = (el: HTMLElement) => {
|
||||||
|
if (el.tagName === 'A') return true;
|
||||||
|
if (el.parentElement) {
|
||||||
|
return isLink(el.parentElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isLink(ev.target as HTMLElement)) return;
|
||||||
|
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return;
|
||||||
|
if (window.getSelection()?.toString() !== '') return;
|
||||||
|
const path = mainRouter.currentRoute.value.path;
|
||||||
|
os.contextMenu([{
|
||||||
|
type: 'label',
|
||||||
|
text: path,
|
||||||
|
}, {
|
||||||
|
icon: 'fas fa-window-maximize',
|
||||||
|
text: i18n.ts.openInWindow,
|
||||||
|
action: () => {
|
||||||
|
os.pageWindow(path);
|
||||||
|
},
|
||||||
|
}], ev);
|
||||||
|
}
|
||||||
|
</script>
|
28
packages/client/src/ui/deckold/mentions-column.vue
Normal file
28
packages/client/src/ui/deckold/mentions-column.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
|
<XNotes :pagination="pagination"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XNotes from '@/components/MkNotes.vue';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
endpoint: 'notes/mentions' as const,
|
||||||
|
limit: 10,
|
||||||
|
};
|
||||||
|
</script>
|
38
packages/client/src/ui/deckold/notifications-column.vue
Normal file
38
packages/client/src/ui/deckold/notifications-column.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @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"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XNotifications from '@/components/MkNotifications.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { updateColumn } from './deck-store';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function func() {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
|
||||||
|
includingTypes: props.column.includingTypes,
|
||||||
|
}, {
|
||||||
|
done: async (res) => {
|
||||||
|
const { includingTypes } = res;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
includingTypes: includingTypes
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
129
packages/client/src/ui/deckold/tl-column.vue
Normal file
129
packages/client/src/ui/deckold/tl-column.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<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)">
|
||||||
|
<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>
|
||||||
|
<i v-else-if="column.tl === 'social'" class="fas fa-share-alt"></i>
|
||||||
|
<i v-else-if="column.tl === 'global'" class="fas fa-globe"></i>
|
||||||
|
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="disabled" class="iwaalbte">
|
||||||
|
<p>
|
||||||
|
<i class="fas fa-minus-circle"></i>
|
||||||
|
{{ $t('disabled-timeline.title') }}
|
||||||
|
</p>
|
||||||
|
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||||
|
</div>
|
||||||
|
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XTimeline from '@/components/MkTimeline.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';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'loaded'): void;
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let disabled = $ref(false);
|
||||||
|
let indicated = $ref(false);
|
||||||
|
let columnActive = $ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.column.tl == null) {
|
||||||
|
setType();
|
||||||
|
} else if ($i) {
|
||||||
|
disabled = !$i.isModerator && !$i.isAdmin && (
|
||||||
|
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
|
||||||
|
instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'local' as const, text: i18n.ts._timelines.local
|
||||||
|
}, {
|
||||||
|
value: 'social' as const, text: i18n.ts._timelines.social
|
||||||
|
}, {
|
||||||
|
value: 'global' as const, text: i18n.ts._timelines.global
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
if (canceled) {
|
||||||
|
if (props.column.tl == null) {
|
||||||
|
removeColumn(props.column.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
tl: src
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueUpdated(q) {
|
||||||
|
if (columnActive) {
|
||||||
|
indicated = q !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNote() {
|
||||||
|
if (!columnActive) {
|
||||||
|
indicated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeActiveState(state) {
|
||||||
|
columnActive = state;
|
||||||
|
|
||||||
|
if (columnActive) {
|
||||||
|
indicated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export default defineComponent({
|
||||||
|
watch: {
|
||||||
|
mediaOnly() {
|
||||||
|
(this.$refs.timeline as any).reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
(this.$refs.timeline as any).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.iwaalbte {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 16px;
|
||||||
|
|
||||||
|
&.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
63
packages/client/src/ui/deckold/widgets-column.vue
Normal file
63
packages/client/src/ui/deckold/widgets-column.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :func="{ handler: func, title: $ts.editWidgets }" :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">
|
||||||
|
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" class="intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
|
||||||
|
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||||
|
</div>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
||||||
|
import XWidgets from '@/components/MkWidgets.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let edit = $ref(false);
|
||||||
|
|
||||||
|
function addWidget(widget) {
|
||||||
|
addColumnWidget(props.column.id, widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWidget(widget) {
|
||||||
|
removeColumnWidget(props.column.id, widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidget({ id, data }) {
|
||||||
|
updateColumnWidget(props.column.id, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidgets(widgets) {
|
||||||
|
setColumnWidgets(props.column.id, widgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
function func() {
|
||||||
|
edit = !edit;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wtdtxvec {
|
||||||
|
--margin: 8px;
|
||||||
|
--panelBorder: none;
|
||||||
|
|
||||||
|
padding: 0 var(--margin);
|
||||||
|
|
||||||
|
> .intro {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main">
|
<div class="main" :class="{ 'noroot': $route.path === '/' }">
|
||||||
<div ref="contents" class="contents" :class="{ wallpaper }">
|
<div ref="contents" class="contents" :class="{ wallpaper }">
|
||||||
<header v-show="mainRouter.currentRoute?.name !== 'index'" ref="header" class="header">
|
<header v-show="mainRouter.currentRoute?.name !== 'index'" ref="header" class="header">
|
||||||
<XHeader :info="pageInfo"/>
|
<XHeader :info="pageInfo"/>
|
||||||
@ -130,6 +130,16 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.noroot {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
|
&.transparent {
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(12px));
|
||||||
|
backdrop-filter: var(--blur, blur(12px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mk-app {
|
.mk-app {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<XKanban class="kanban" full/>
|
<XKanban class="kanban" full/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main">
|
<div class="main" :class="{ 'noroot': !root }">
|
||||||
<XKanban v-if="narrow && !root" class="banner" :powered-by="root"/>
|
<XKanban v-if="narrow && !root" class="banner" :powered-by="root"/>
|
||||||
|
|
||||||
<div class="contents">
|
<div class="contents">
|
||||||
@ -127,6 +127,14 @@ defineExpose({
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.noroot {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
&.transparent {
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(12px));
|
||||||
|
backdrop-filter: var(--blur, blur(12px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tray-enter-active,
|
.tray-enter-active,
|
||||||
.tray-leave-active {
|
.tray-leave-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -1226,6 +1226,11 @@ delayed-stream@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||||
|
|
||||||
|
destyle.css@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/destyle.css/-/destyle.css-3.0.2.tgz#80c331db91f02337040b62dc534ab4f3dc075546"
|
||||||
|
integrity sha512-OUzuAlridP01YP0seHxx+FgwhdXF07Ls9NxnJpR8EI7ADioswoR7VKkBd/s1hNw/G4ILasLSA0k2Kw1M57r1kA==
|
||||||
|
|
||||||
detect-node@2.1.0, detect-node@^2.1.0:
|
detect-node@2.1.0, detect-node@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
||||||
@ -2616,6 +2621,13 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
|||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
|
"misetehoshii@https://github.com/melt-adzuki/misetehoshii":
|
||||||
|
version "0.0.0"
|
||||||
|
resolved "https://github.com/melt-adzuki/misetehoshii#d00f51529e390808f92172000f960b4f1314f37c"
|
||||||
|
dependencies:
|
||||||
|
destyle.css "^3.0.2"
|
||||||
|
vue "^3.2.37"
|
||||||
|
|
||||||
misskey-js@0.0.14:
|
misskey-js@0.0.14:
|
||||||
version "0.0.14"
|
version "0.0.14"
|
||||||
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
|
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
|
||||||
@ -3621,7 +3633,7 @@ vue-prism-editor@2.0.0-alpha.2:
|
|||||||
resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
|
resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
|
||||||
integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
|
integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
|
||||||
|
|
||||||
vue@3.2.39:
|
vue@3.2.39, vue@^3.2.37:
|
||||||
version "3.2.39"
|
version "3.2.39"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.39.tgz#de071c56c4c32c41cbd54e55f11404295c0dd62d"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.39.tgz#de071c56c4c32c41cbd54e55f11404295c0dd62d"
|
||||||
integrity sha512-tRkguhRTw9NmIPXhzk21YFBqXHT2t+6C6wPOgQ50fcFVWnPdetmRqbmySRHznrYjX2E47u0cGlKGcxKZJ38R/g==
|
integrity sha512-tRkguhRTw9NmIPXhzk21YFBqXHT2t+6C6wPOgQ50fcFVWnPdetmRqbmySRHznrYjX2E47u0cGlKGcxKZJ38R/g==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user