summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-07-15 22:45:13 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-07-15 22:45:13 +0900
commit4c8a1867f09efe6b2475e37efa1dce5d89c56a54 (patch)
tree1d97e465f7dd0d4814568db8670ef4588ac48ef6 /packages
parentMerge branch 'develop' (diff)
parent12.114.0 (diff)
downloadmisskey-4c8a1867f09efe6b2475e37efa1dce5d89c56a54.tar.gz
misskey-4c8a1867f09efe6b2475e37efa1dce5d89c56a54.tar.bz2
misskey-4c8a1867f09efe6b2475e37efa1dce5d89c56a54.zip
Merge branch 'develop'
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/src/server/web/boot.js89
-rw-r--r--packages/client/package.json2
-rw-r--r--packages/client/src/components/drive.vue61
-rw-r--r--packages/client/src/components/global/loading.vue4
-rw-r--r--packages/client/src/components/launch-pad.vue4
-rw-r--r--packages/client/src/components/mention.vue4
-rw-r--r--packages/client/src/components/note.vue2
-rw-r--r--packages/client/src/components/notification.vue8
-rw-r--r--packages/client/src/components/signup.vue411
-rw-r--r--packages/client/src/components/ui/menu.vue2
-rw-r--r--packages/client/src/components/ui/modal-window.vue3
-rw-r--r--packages/client/src/components/ui/tooltip.vue2
-rw-r--r--packages/client/src/components/ui/window.vue2
-rw-r--r--packages/client/src/directives/index.ts2
-rw-r--r--packages/client/src/directives/sticky-container.ts17
-rw-r--r--packages/client/src/navbar.ts (renamed from packages/client/src/menu.ts)2
-rw-r--r--packages/client/src/pages/settings/general.vue8
-rw-r--r--packages/client/src/pages/settings/index.vue16
-rw-r--r--packages/client/src/pages/settings/navbar.vue (renamed from packages/client/src/pages/settings/menu.vue)10
-rw-r--r--packages/client/src/pages/settings/security.vue2
-rw-r--r--packages/client/src/pages/settings/statusbars.statusbar.vue5
-rw-r--r--packages/client/src/pages/user/index.timeline.vue28
-rw-r--r--packages/client/src/scripts/shuffle.ts19
-rw-r--r--packages/client/src/style.scss14
-rw-r--r--packages/client/src/ui/_common_/navbar-for-mobile.vue284
-rw-r--r--packages/client/src/ui/_common_/navbar.vue492
-rw-r--r--packages/client/src/ui/_common_/sidebar-for-mobile.vue209
-rw-r--r--packages/client/src/ui/_common_/sidebar.vue303
-rw-r--r--packages/client/src/ui/_common_/statusbar-rss.vue5
-rw-r--r--packages/client/src/ui/_common_/statusbars.vue3
-rw-r--r--packages/client/src/ui/classic.header.vue16
-rw-r--r--packages/client/src/ui/classic.sidebar.vue16
-rw-r--r--packages/client/src/ui/classic.vue1
-rw-r--r--packages/client/src/ui/deck.vue13
-rw-r--r--packages/client/src/ui/deck/column.vue3
-rw-r--r--packages/client/src/ui/universal.vue14
-rw-r--r--packages/client/src/widgets/rss-ticker.vue8
-rw-r--r--packages/client/vite.json5.ts52
38 files changed, 1203 insertions, 933 deletions
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 0a5cc0e0dc..9570115423 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -14,9 +14,11 @@
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => {
window.onerror = (e) => {
+ console.error(e);
renderError('SOMETHING_HAPPENED', e);
};
window.onunhandledrejection = (e) => {
+ console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
};
@@ -47,18 +49,30 @@
localStorage.setItem('localeVersion', v);
} else {
await checkUpdate();
- renderError('LOCALE_FETCH_FAILED');
+ renderError('LOCALE_FETCH');
return;
}
}
//#endregion
//#region Script
- import(`/assets/${CLIENT_ENTRY}`)
- .catch(async e => {
- await checkUpdate();
- renderError('APP_FETCH_FAILED', e);
- })
+ function importAppScript() {
+ import(`/assets/${CLIENT_ENTRY}`)
+ .catch(async e => {
+ await checkUpdate();
+ console.error(e);
+ renderError('APP_IMPORT', e);
+ });
+ }
+
+ // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
+ if (document.readyState !== 'loading') {
+ importAppScript();
+ } else {
+ window.addEventListener('DOMContentLoaded', () => {
+ importAppScript();
+ });
+ }
//#endregion
//#region Theme
@@ -112,35 +126,35 @@
let errorsElement = document.getElementById('errors');
if (!errorsElement) {
- document.documentElement.innerHTML = `
+ document.body.innerHTML = `
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
- <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
- <path d="M12 9v2m0 4v.01"></path>
- <path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
+ <path d="M12 9v2m0 4v.01"></path>
+ <path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
</svg>
<h1>An error has occurred!</h1>
<button class="button-big" onclick="location.reload(true);">
<span class="button-label-big">Refresh</span>
</button>
- <p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
+ <p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
- <a href="/flush">
- <button class="button-small">
- <span class="button-label-small">Clear preferences and cache</span>
- </button>
- </a>
+ <a href="/flush">
+ <button class="button-small">
+ <span class="button-label-small">Clear preferences and cache</span>
+ </button>
+ </a>
<br>
- <a href="/cli">
- <button class="button-small">
- <span class="button-label-small">Start the simple client</span>
- </button>
- </a>
+ <a href="/cli">
+ <button class="button-small">
+ <span class="button-label-small">Start the simple client</span>
+ </button>
+ </a>
<br>
- <a href="/bios">
- <button class="button-small">
- <span class="button-label-small">Start the repair tool</span>
- </button>
- </a>
+ <a href="/bios">
+ <button class="button-small">
+ <span class="button-label-small">Start the repair tool</span>
+ </button>
+ </a>
<br>
<div id="errors"></div>
`;
@@ -269,17 +283,22 @@
// eslint-disable-next-line no-inner-declarations
async function checkUpdate() {
- // TODO: サーバーが落ちている場合などのエラーハンドリング
- const res = await fetch('/api/meta', {
- method: 'POST',
- cache: 'no-cache'
- });
+ try {
+ const res = await fetch('/api/meta', {
+ method: 'POST',
+ cache: 'no-cache'
+ });
- const meta = await res.json();
+ const meta = await res.json();
- if (meta.version != v) {
- localStorage.setItem('v', meta.version);
- refresh();
+ if (meta.version != v) {
+ localStorage.setItem('v', meta.version);
+ refresh();
+ }
+ } catch (e) {
+ console.error(e);
+ renderError('UPDATE_CHECK', e);
+ throw e;
}
}
diff --git a/packages/client/package.json b/packages/client/package.json
index 8d2efd0b92..94f6688dd2 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -56,7 +56,6 @@
"random-seed": "0.3.0",
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
- "rollup": "2.76.0",
"s-age": "1.1.2",
"sass": "1.53.0",
"seedrandom": "3.0.5",
@@ -102,6 +101,7 @@
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
+ "rollup": "2.76.0",
"cross-env": "7.0.3",
"cypress": "10.3.0",
"eslint": "8.19.0",
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
index 6c2c8acad0..9e2ef1b930 100644
--- a/packages/client/src/components/drive.vue
+++ b/packages/client/src/components/drive.vue
@@ -26,7 +26,8 @@
</div>
<button class="menu _button" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
</nav>
- <div ref="main" class="main"
+ <div
+ ref="main" class="main"
:class="{ uploading: uploadings.length > 0, fetching }"
@dragover.prevent.stop="onDragover"
@dragenter="onDragenter"
@@ -142,7 +143,7 @@ const isDragSource = ref(false);
const fetching = ref(true);
const ilFilesObserver = new IntersectionObserver(
- (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
+ (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
);
watch(folder, () => emit('cd', folder.value));
@@ -232,7 +233,7 @@ function onDrop(ev: DragEvent): any {
removeFile(file.id);
os.api('drive/files/update', {
fileId: file.id,
- folderId: folder.value ? folder.value.id : null
+ folderId: folder.value ? folder.value.id : null,
});
}
//#endregion
@@ -248,7 +249,7 @@ function onDrop(ev: DragEvent): any {
removeFolder(droppedFolder.id);
os.api('drive/folders/update', {
folderId: droppedFolder.id,
- parentId: folder.value ? folder.value.id : null
+ parentId: folder.value ? folder.value.id : null,
}).then(() => {
// noop
}).catch(err => {
@@ -256,13 +257,13 @@ function onDrop(ev: DragEvent): any {
case 'detected-circular-definition':
os.alert({
title: i18n.ts.unableToProcess,
- text: i18n.ts.circularReferenceFolder
+ text: i18n.ts.circularReferenceFolder,
});
break;
default:
os.alert({
type: 'error',
- text: i18n.ts.somethingHappened
+ text: i18n.ts.somethingHappened,
});
}
});
@@ -278,17 +279,17 @@ function urlUpload() {
os.inputText({
title: i18n.ts.uploadFromUrl,
type: 'url',
- placeholder: i18n.ts.uploadFromUrlDescription
+ placeholder: i18n.ts.uploadFromUrlDescription,
}).then(({ canceled, result: url }) => {
if (canceled || !url) return;
os.api('drive/files/upload-from-url', {
url: url,
- folderId: folder.value ? folder.value.id : undefined
+ folderId: folder.value ? folder.value.id : undefined,
});
os.alert({
title: i18n.ts.uploadFromUrlRequested,
- text: i18n.ts.uploadFromUrlMayTakeTime
+ text: i18n.ts.uploadFromUrlMayTakeTime,
});
});
}
@@ -296,12 +297,12 @@ function urlUpload() {
function createFolder() {
os.inputText({
title: i18n.ts.createFolder,
- placeholder: i18n.ts.folderName
+ placeholder: i18n.ts.folderName,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/create', {
name: name,
- parentId: folder.value ? folder.value.id : undefined
+ parentId: folder.value ? folder.value.id : undefined,
}).then(createdFolder => {
addFolder(createdFolder, true);
});
@@ -312,12 +313,12 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
os.inputText({
title: i18n.ts.renameFolder,
placeholder: i18n.ts.inputNewFolderName,
- default: folderToRename.name
+ default: folderToRename.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/update', {
folderId: folderToRename.id,
- name: name
+ name: name,
}).then(updatedFolder => {
// FIXME: 画面を更新するために自分自身に移動
move(updatedFolder);
@@ -327,7 +328,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
os.api('drive/folders/delete', {
- folderId: folderToDelete.id
+ folderId: folderToDelete.id,
}).then(() => {
// 削除時に親フォルダに移動
move(folderToDelete.parentId);
@@ -337,15 +338,15 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
os.alert({
type: 'error',
title: i18n.ts.unableToDelete,
- text: i18n.ts.hasChildFilesOrFolders
+ text: i18n.ts.hasChildFilesOrFolders,
});
break;
default:
os.alert({
type: 'error',
- text: i18n.ts.unableToDelete
+ text: i18n.ts.unableToDelete,
});
- }
+ }
});
}
@@ -411,7 +412,7 @@ function move(target?: Misskey.entities.DriveFolder) {
fetching.value = true;
os.api('drive/folders/show', {
- folderId: target
+ folderId: target,
}).then(folderToMove => {
folder.value = folderToMove;
hierarchyFolders.value = [];
@@ -510,7 +511,7 @@ async function fetch() {
const foldersPromise = os.api('drive/folders', {
folderId: folder.value ? folder.value.id : null,
- limit: foldersMax + 1
+ limit: foldersMax + 1,
}).then(fetchedFolders => {
if (fetchedFolders.length === foldersMax + 1) {
moreFolders.value = true;
@@ -522,7 +523,7 @@ async function fetch() {
const filesPromise = os.api('drive/files', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
- limit: filesMax + 1
+ limit: filesMax + 1,
}).then(fetchedFiles => {
if (fetchedFiles.length === filesMax + 1) {
moreFiles.value = true;
@@ -549,7 +550,7 @@ function fetchMoreFiles() {
folderId: folder.value ? folder.value.id : null,
type: props.type,
untilId: files.value[files.value.length - 1].id,
- limit: max + 1
+ limit: max + 1,
}).then(files => {
if (files.length === max + 1) {
moreFiles.value = true;
@@ -569,30 +570,30 @@ function getMenu() {
ref: keepOriginal,
}, null, {
text: i18n.ts.addFile,
- type: 'label'
+ type: 'label',
}, {
text: i18n.ts.upload,
icon: 'fas fa-upload',
- action: () => { selectLocalFile(); }
+ action: () => { selectLocalFile(); },
}, {
text: i18n.ts.fromUrl,
icon: 'fas fa-link',
- action: () => { urlUpload(); }
+ action: () => { urlUpload(); },
}, null, {
text: folder.value ? folder.value.name : i18n.ts.drive,
- type: 'label'
+ type: 'label',
}, folder.value ? {
text: i18n.ts.renameFolder,
icon: 'fas fa-i-cursor',
- action: () => { renameFolder(folder.value); }
+ action: () => { renameFolder(folder.value); },
} : undefined, folder.value ? {
text: i18n.ts.deleteFolder,
icon: 'fas fa-trash-alt',
- action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
+ action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
} : undefined, {
text: i18n.ts.createFolder,
icon: 'fas fa-folder-plus',
- action: () => { createFolder(); }
+ action: () => { createFolder(); },
}];
}
@@ -662,14 +663,14 @@ onBeforeUnmount(() => {
> .path {
display: inline-block;
vertical-align: bottom;
- line-height: 50px;
+ line-height: 42px;
white-space: nowrap;
> * {
display: inline-block;
margin: 0;
padding: 0 8px;
- line-height: 50px;
+ line-height: 42px;
cursor: pointer;
* {
diff --git a/packages/client/src/components/global/loading.vue b/packages/client/src/components/global/loading.vue
index 5a7e362fcf..bcc6dfac01 100644
--- a/packages/client/src/components/global/loading.vue
+++ b/packages/client/src/components/global/loading.vue
@@ -16,9 +16,7 @@
</template>
<script lang="ts" setup>
-import { useCssModule } from 'vue';
-
-useCssModule();
+import { } from 'vue';
const props = withDefaults(defineProps<{
inline?: boolean;
diff --git a/packages/client/src/components/launch-pad.vue b/packages/client/src/components/launch-pad.vue
index a6025f8b27..4693df2916 100644
--- a/packages/client/src/components/launch-pad.vue
+++ b/packages/client/src/components/launch-pad.vue
@@ -36,7 +36,7 @@
<script lang="ts" setup>
import { } from 'vue';
import MkModal from '@/components/ui/modal.vue';
-import { menuDef } from '@/menu';
+import { navbarItemDef } from '@/navbar';
import { instanceName } from '@/config';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -62,7 +62,7 @@ const modal = $ref<InstanceType<typeof MkModal>>();
const menu = defaultStore.state.menu;
-const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
+const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
type: def.to ? 'link' : 'button',
text: i18n.ts[def.title],
icon: def.icon,
diff --git a/packages/client/src/components/mention.vue b/packages/client/src/components/mention.vue
index cf69437771..3091b435e4 100644
--- a/packages/client/src/components/mention.vue
+++ b/packages/client/src/components/mention.vue
@@ -16,7 +16,7 @@
<script lang="ts" setup>
import { toUnicode } from 'punycode';
-import { useCssModule } from 'vue';
+import { } from 'vue';
import tinycolor from 'tinycolor2';
import { host as localHost } from '@/config';
import { $i } from '@/account';
@@ -37,8 +37,6 @@ const isMe = $i && (
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
-
-useCssModule();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
index c96cdddef2..27716cf73d 100644
--- a/packages/client/src/components/note.vue
+++ b/packages/client/src/components/note.vue
@@ -592,8 +592,6 @@ function readPromo() {
}
&.max-width_300px {
- font-size: 0.825em;
-
> .article {
> .avatar {
width: 44px;
diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue
index 32f9fd07d8..10cbe20902 100644
--- a/packages/client/src/components/notification.vue
+++ b/packages/client/src/components/notification.vue
@@ -177,13 +177,7 @@ useTooltip(reactionRef, (showing) => {
&.max-width_500px {
padding: 12px;
- font-size: 0.8em;
- }
-
- &:after {
- content: "";
- display: block;
- clear: both;
+ font-size: 0.85em;
}
> .head {
diff --git a/packages/client/src/components/signup.vue b/packages/client/src/components/signup.vue
index dd4a2b18b8..c35d65d5de 100644
--- a/packages/client/src/components/signup.vue
+++ b/packages/client/src/components/signup.vue
@@ -1,255 +1,234 @@
<template>
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
- <template v-if="meta">
- <MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
- <template #label>{{ $ts.invitationCode }}</template>
- <template #prefix><i class="fas fa-key"></i></template>
- </MkInput>
- <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
- <template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
- <template #prefix>@</template>
- <template #suffix>@{{ host }}</template>
- <template #caption>
- <span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
- <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
- <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
- <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
- <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
- <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
- <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
+ <MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
+ <template #label>{{ $ts.invitationCode }}</template>
+ <template #prefix><i class="fas fa-key"></i></template>
+ </MkInput>
+ <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
+ <template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
+ <template #prefix>@</template>
+ <template #suffix>@{{ host }}</template>
+ <template #caption>
+ <span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
+ <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
+ <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
+ <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
+ <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
+ <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
+ <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
+ </template>
+ </MkInput>
+ <MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
+ <template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
+ <template #prefix><i class="fas fa-envelope"></i></template>
+ <template #caption>
+ <span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
+ <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
+ <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
+ <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
+ <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
+ <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
+ <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
+ <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
+ <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
+ </template>
+ </MkInput>
+ <MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
+ <template #label>{{ $ts.password }}</template>
+ <template #prefix><i class="fas fa-lock"></i></template>
+ <template #caption>
+ <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
+ <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
+ <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
+ </template>
+ </MkInput>
+ <MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
+ <template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
+ <template #prefix><i class="fas fa-lock"></i></template>
+ <template #caption>
+ <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
+ <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
+ </template>
+ </MkInput>
+ <MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
+ <I18n :src="$ts.agreeTo">
+ <template #0>
+ <a :href="instance.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
</template>
- </MkInput>
- <MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
- <template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
- <template #prefix><i class="fas fa-envelope"></i></template>
- <template #caption>
- <span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
- <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
- <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
- <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
- <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
- <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
- <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
- <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
- <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
- </template>
- </MkInput>
- <MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
- <template #label>{{ $ts.password }}</template>
- <template #prefix><i class="fas fa-lock"></i></template>
- <template #caption>
- <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
- <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
- <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
- </template>
- </MkInput>
- <MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
- <template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
- <template #prefix><i class="fas fa-lock"></i></template>
- <template #caption>
- <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
- <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
- </template>
- </MkInput>
- <MkSwitch v-if="meta.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
- <I18n :src="$ts.agreeTo">
- <template #0>
- <a :href="meta.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
- </template>
- </I18n>
- </MkSwitch>
- <MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
- <MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
- <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
- </template>
+ </I18n>
+ </MkSwitch>
+ <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+ <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+ <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</form>
</template>
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import getPasswordStrength from 'syuilo-password-strength';
import { toUnicode } from 'punycode/';
import MkButton from './ui/button.vue';
+import MkCaptcha from './captcha.vue';
import MkInput from './form/input.vue';
import MkSwitch from './form/switch.vue';
-import { host, url } from '@/config';
+import * as config from '@/config';
import * as os from '@/os';
import { login } from '@/account';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkButton,
- MkInput,
- MkSwitch,
- MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')),
- },
-
- props: {
- autoSet: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
+const props = withDefaults(defineProps<{
+ autoSet?: boolean;
+}>(), {
+ autoSet: false,
+});
- emits: ['signup'],
+const emit = defineEmits<{
+ (ev: 'signup', user: Record<string, any>): void;
+ (ev: 'signupEmailPending'): void;
+}>();
- data() {
- return {
- host: toUnicode(host),
- username: '',
- password: '',
- retypedPassword: '',
- invitationCode: '',
- email: '',
- url,
- usernameState: null,
- emailState: null,
- passwordStrength: '',
- passwordRetypeState: null,
- submitting: false,
- ToSAgreement: false,
- hCaptchaResponse: null,
- reCaptchaResponse: null,
- };
- },
+const host = toUnicode(config.host);
- computed: {
- meta() {
- return this.$instance;
- },
+let hcaptcha = $ref();
+let recaptcha = $ref();
- shouldDisableSubmitting(): boolean {
- return this.submitting ||
- this.meta.tosUrl && !this.ToSAgreement ||
- this.meta.enableHcaptcha && !this.hCaptchaResponse ||
- this.meta.enableRecaptcha && !this.reCaptchaResponse ||
- this.passwordRetypeState === 'not-match';
- },
+let username: string = $ref('');
+let password: string = $ref('');
+let retypedPassword: string = $ref('');
+let invitationCode: string = $ref('');
+let email = $ref('');
+let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
+let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
+let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
+let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
+let submitting: boolean = $ref(false);
+let ToSAgreement: boolean = $ref(false);
+let hCaptchaResponse = $ref(null);
+let reCaptchaResponse = $ref(null);
- shouldShowProfileUrl(): boolean {
- return (this.username !== '' &&
- this.usernameState !== 'invalid-format' &&
- this.usernameState !== 'min-range' &&
- this.usernameState !== 'max-range');
- },
- },
+const shouldDisableSubmitting = $computed((): boolean => {
+ return submitting ||
+ instance.tosUrl && !ToSAgreement ||
+ instance.enableHcaptcha && !hCaptchaResponse ||
+ instance.enableRecaptcha && !reCaptchaResponse ||
+ passwordRetypeState === 'not-match';
+});
- methods: {
- onChangeUsername() {
- if (this.username === '') {
- this.usernameState = null;
- return;
- }
+function onChangeUsername(): void {
+ if (username === '') {
+ usernameState = null;
+ return;
+ }
- const err =
- !this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
- this.username.length < 1 ? 'min-range' :
- this.username.length > 20 ? 'max-range' :
- null;
+ {
+ const err =
+ !username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
+ username.length < 1 ? 'min-range' :
+ username.length > 20 ? 'max-range' :
+ null;
- if (err) {
- this.usernameState = err;
- return;
- }
+ if (err) {
+ usernameState = err;
+ return;
+ }
+ }
- this.usernameState = 'wait';
+ usernameState = 'wait';
- os.api('username/available', {
- username: this.username,
- }).then(result => {
- this.usernameState = result.available ? 'ok' : 'unavailable';
- }).catch(err => {
- this.usernameState = 'error';
- });
- },
+ os.api('username/available', {
+ username,
+ }).then(result => {
+ usernameState = result.available ? 'ok' : 'unavailable';
+ }).catch(() => {
+ usernameState = 'error';
+ });
+}
- onChangeEmail() {
- if (this.email === '') {
- this.emailState = null;
- return;
- }
+function onChangeEmail(): void {
+ if (email === '') {
+ emailState = null;
+ return;
+ }
- this.emailState = 'wait';
+ emailState = 'wait';
- os.api('email-address/available', {
- emailAddress: this.email,
- }).then(result => {
- this.emailState = result.available ? 'ok' :
- result.reason === 'used' ? 'unavailable:used' :
- result.reason === 'format' ? 'unavailable:format' :
- result.reason === 'disposable' ? 'unavailable:disposable' :
- result.reason === 'mx' ? 'unavailable:mx' :
- result.reason === 'smtp' ? 'unavailable:smtp' :
- 'unavailable';
- }).catch(err => {
- this.emailState = 'error';
- });
- },
+ os.api('email-address/available', {
+ emailAddress: email,
+ }).then(result => {
+ emailState = result.available ? 'ok' :
+ result.reason === 'used' ? 'unavailable:used' :
+ result.reason === 'format' ? 'unavailable:format' :
+ result.reason === 'disposable' ? 'unavailable:disposable' :
+ result.reason === 'mx' ? 'unavailable:mx' :
+ result.reason === 'smtp' ? 'unavailable:smtp' :
+ 'unavailable';
+ }).catch(() => {
+ emailState = 'error';
+ });
+}
- onChangePassword() {
- if (this.password === '') {
- this.passwordStrength = '';
- return;
- }
+function onChangePassword(): void {
+ if (password === '') {
+ passwordStrength = '';
+ return;
+ }
- const strength = getPasswordStrength(this.password);
- this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
- },
+ const strength = getPasswordStrength(password);
+ passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
+}
- onChangePasswordRetype() {
- if (this.retypedPassword === '') {
- this.passwordRetypeState = null;
- return;
- }
+function onChangePasswordRetype(): void {
+ if (retypedPassword === '') {
+ passwordRetypeState = null;
+ return;
+ }
- this.passwordRetypeState = this.password === this.retypedPassword ? 'match' : 'not-match';
- },
+ passwordRetypeState = password === retypedPassword ? 'match' : 'not-match';
+}
- onSubmit() {
- if (this.submitting) return;
- this.submitting = true;
+function onSubmit(): void {
+ if (submitting) return;
+ submitting = true;
- os.api('signup', {
- username: this.username,
- password: this.password,
- emailAddress: this.email,
- invitationCode: this.invitationCode,
- 'hcaptcha-response': this.hCaptchaResponse,
- 'g-recaptcha-response': this.reCaptchaResponse,
- }).then(() => {
- if (this.meta.emailRequiredForSignup) {
- os.alert({
- type: 'success',
- title: this.$ts._signup.almostThere,
- text: this.$t('_signup.emailSent', { email: this.email }),
- });
- this.$emit('signupEmailPending');
- } else {
- os.api('signin', {
- username: this.username,
- password: this.password,
- }).then(res => {
- this.$emit('signup', res);
+ os.api('signup', {
+ username,
+ password,
+ emailAddress: email,
+ invitationCode,
+ 'hcaptcha-response': hCaptchaResponse,
+ 'g-recaptcha-response': reCaptchaResponse,
+ }).then(() => {
+ if (instance.emailRequiredForSignup) {
+ os.alert({
+ type: 'success',
+ title: i18n.ts._signup.almostThere,
+ text: i18n.t('_signup.emailSent', { email }),
+ });
+ emit('signupEmailPending');
+ } else {
+ os.api('signin', {
+ username,
+ password,
+ }).then(res => {
+ emit('signup', res);
- if (this.autoSet) {
- login(res.i);
- }
- });
+ if (props.autoSet) {
+ login(res.i);
}
- }).catch(() => {
- this.submitting = false;
- this.$refs.hcaptcha?.reset?.();
- this.$refs.recaptcha?.reset?.();
-
- os.alert({
- type: 'error',
- text: this.$ts.somethingHappened,
- });
});
- },
- },
-});
+ }
+ }).catch(() => {
+ submitting = false;
+ hcaptcha.reset?.();
+ recaptcha.reset?.();
+
+ os.alert({
+ type: 'error',
+ text: i18n.ts.somethingHappened,
+ });
+ });
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 1f3d508975..6ad63c2ad7 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -140,7 +140,7 @@ function focusDown() {
width: 100%;
box-sizing: border-box;
white-space: nowrap;
- font-size: 0.85em;
+ font-size: 0.9em;
line-height: 20px;
text-align: left;
overflow: hidden;
diff --git a/packages/client/src/components/ui/modal-window.vue b/packages/client/src/components/ui/modal-window.vue
index b7faea736b..b29ea4fd81 100644
--- a/packages/client/src/components/ui/modal-window.vue
+++ b/packages/client/src/components/ui/modal-window.vue
@@ -98,7 +98,7 @@ defineExpose({
}
> .header {
- $height: 58px;
+ $height: 46px;
$height-narrow: 42px;
display: flex;
flex-shrink: 0;
@@ -138,6 +138,7 @@ defineExpose({
}
> .body {
+ flex: 1;
overflow: auto;
background: var(--panel);
}
diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue
index 152c939a1a..f81bf2fc5b 100644
--- a/packages/client/src/components/ui/tooltip.vue
+++ b/packages/client/src/components/ui/tooltip.vue
@@ -116,7 +116,7 @@ const setPosition = () => {
let top: number;
if (props.targetElement) {
- left = (rect.left + window.pageXOffset) + props.innerMargin;
+ left = (rect.left + props.targetElement.offsetWidth + window.pageXOffset) + props.innerMargin;
top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
} else {
left = props.x + props.innerMargin;
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index 6892b1924e..d155033824 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -393,7 +393,7 @@ export default defineComponent({
border-radius: var(--radius);
> .header {
- --height: 45px;
+ --height: 42px;
&.mini {
--height: 38px;
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index fc9b6f86da..401a917cba 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -8,7 +8,6 @@ import tooltip from './tooltip';
import hotkey from './hotkey';
import appear from './appear';
import anim from './anim';
-import stickyContainer from './sticky-container';
import clickAnime from './click-anime';
import panel from './panel';
import adaptiveBorder from './adaptive-border';
@@ -24,7 +23,6 @@ export default function(app: App) {
app.directive('appear', appear);
app.directive('anim', anim);
app.directive('click-anime', clickAnime);
- app.directive('sticky-container', stickyContainer);
app.directive('panel', panel);
app.directive('adaptive-border', adaptiveBorder);
}
diff --git a/packages/client/src/directives/sticky-container.ts b/packages/client/src/directives/sticky-container.ts
deleted file mode 100644
index 3cf813054b..0000000000
--- a/packages/client/src/directives/sticky-container.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Directive } from 'vue';
-
-export default {
- mounted(src, binding, vn) {
- //const query = binding.value;
-
- const header = src.children[0];
- const body = src.children[1];
- const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
- src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
- if (body) body.dataset.stickyContainerHeaderHeight = header.offsetHeight.toString();
- header.style.setProperty('--stickyTop', currentStickyTop);
- header.style.position = 'sticky';
- header.style.top = 'var(--stickyTop)';
- header.style.zIndex = '1';
- },
-} as Directive;
diff --git a/packages/client/src/menu.ts b/packages/client/src/navbar.ts
index 31b2ed597c..03e00b1c17 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/navbar.ts
@@ -6,7 +6,7 @@ import { i18n } from '@/i18n';
import { ui } from '@/config';
import { unisonReload } from '@/scripts/unison-reload';
-export const menuDef = reactive({
+export const navbarItemDef = reactive({
notifications: {
title: 'notifications',
icon: 'fas fa-bell',
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
index 74fa0bc926..cd2bcc581a 100644
--- a/packages/client/src/pages/settings/general.vue
+++ b/packages/client/src/pages/settings/general.vue
@@ -56,10 +56,10 @@
<FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ i18n.ts.fontSize }}</template>
- <option value="small"><span style="font-size: 14px;">Aa</span></option>
- <option :value="null"><span style="font-size: 16px;">Aa</span></option>
- <option value="large"><span style="font-size: 18px;">Aa</span></option>
- <option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
+ <option :value="null"><span style="font-size: 14px;">Aa</span></option>
+ <option value="1"><span style="font-size: 15px;">Aa</span></option>
+ <option value="2"><span style="font-size: 16px;">Aa</span></option>
+ <option value="3"><span style="font-size: 17px;">Aa</span></option>
</FormRadios>
</FormSection>
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 76410ec12f..f970660a4a 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -114,16 +114,16 @@ const menuDef = computed(() => [{
to: '/settings/theme',
active: props.initialPage === 'theme',
}, {
- icon: 'fas fa-list-ul',
+ icon: 'fas fa-bars',
+ text: i18n.ts.navbar,
+ to: '/settings/navbar',
+ active: props.initialPage === 'navbar',
+ }, {
+ icon: 'fas fa-bars-progress',
text: i18n.ts.statusbar,
to: '/settings/statusbars',
active: props.initialPage === 'statusbars',
}, {
- icon: 'fas fa-list-ul',
- text: i18n.ts.menu,
- to: '/settings/menu',
- active: props.initialPage === 'menu',
- }, {
icon: 'fas fa-music',
text: i18n.ts.sounds,
to: '/settings/sounds',
@@ -225,7 +225,7 @@ const component = computed(() => {
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
- case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
+ case 'navbar': return defineAsyncComponent(() => import('./navbar.vue'));
case 'statusbars': return defineAsyncComponent(() => import('./statusbars.vue'));
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
@@ -291,6 +291,8 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(INFO);
+// w 890
+// h 700
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/navbar.vue
index 076654c105..534112c3e0 100644
--- a/packages/client/src/pages/settings/menu.vue
+++ b/packages/client/src/pages/settings/navbar.vue
@@ -1,7 +1,7 @@
<template>
<div class="_formRoot">
<FormTextarea v-model="items" tall manual-save class="_formBlock">
- <template #label>{{ i18n.ts.menu }}</template>
+ <template #label>{{ i18n.ts.navbar }}</template>
<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
</FormTextarea>
@@ -23,7 +23,7 @@ import FormTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
-import { menuDef } from '@/menu';
+import { navbarItemDef } from '@/navbar';
import { defaultStore } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
@@ -45,11 +45,11 @@ async function reloadAsk() {
}
async function addItem() {
- const menu = Object.keys(menuDef).filter(k => !defaultStore.state.menu.includes(k));
+ const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
const { canceled, result: item } = await os.select({
title: i18n.ts.addItem,
items: [...menu.map(k => ({
- value: k, text: i18n.ts[menuDef[k].title],
+ value: k, text: i18n.ts[navbarItemDef[k].title],
})), {
value: '-', text: i18n.ts.divider,
}],
@@ -81,7 +81,7 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
- title: i18n.ts.menu,
+ title: i18n.ts.navbar,
icon: 'fas fa-list-ul',
});
</script>
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
index eb3efa9afb..d926acce5d 100644
--- a/packages/client/src/pages/settings/security.vue
+++ b/packages/client/src/pages/settings/security.vue
@@ -12,7 +12,7 @@
<FormSection>
<template #label>{{ i18n.ts.signinHistory }}</template>
- <MkPagination :pagination="pagination">
+ <MkPagination :pagination="pagination" disable-auto-load>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
diff --git a/packages/client/src/pages/settings/statusbars.statusbar.vue b/packages/client/src/pages/settings/statusbars.statusbar.vue
index 206979925e..2f0c6fc1ee 100644
--- a/packages/client/src/pages/settings/statusbars.statusbar.vue
+++ b/packages/client/src/pages/settings/statusbars.statusbar.vue
@@ -28,6 +28,9 @@
<MkInput v-model="statusbar.props.url" manual-save class="_formBlock" type="url">
<template #label>URL</template>
</MkInput>
+ <MkSwitch v-model="statusbar.props.shuffle" class="_formBlock">
+ <template #label>{{ i18n.ts.shuffle }}</template>
+ </MkSwitch>
<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
<template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput>
@@ -86,7 +89,6 @@ import FormRadios from '@/components/form/radios.vue';
import FormButton from '@/components/ui/button.vue';
import FormRange from '@/components/form/range.vue';
import * as os from '@/os';
-import { menuDef } from '@/menu';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -101,6 +103,7 @@ watch(() => statusbar.type, () => {
if (statusbar.type === 'rss') {
statusbar.name = 'NEWS';
statusbar.props.url = 'http://feeds.afpbb.com/rss/afpbb/afpbbnews';
+ statusbar.props.shuffle = true;
statusbar.props.refreshIntervalSec = 120;
statusbar.props.display = 'marquee';
statusbar.props.marqueeDuration = 100;
diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue
index a1329a7411..1bcc0a1b85 100644
--- a/packages/client/src/pages/user/index.timeline.vue
+++ b/packages/client/src/pages/user/index.timeline.vue
@@ -1,12 +1,14 @@
<template>
-<div v-sticky-container class="yrzkoczt">
- <MkTab v-model="include" class="tab">
- <option :value="null">{{ $ts.notes }}</option>
- <option value="replies">{{ $ts.notesAndReplies }}</option>
- <option value="files">{{ $ts.withFiles }}</option>
- </MkTab>
+<MkStickyContainer>
+ <template #header>
+ <MkTab v-model="include" :class="$style.tab">
+ <option :value="null">{{ $ts.notes }}</option>
+ <option value="replies">{{ $ts.notesAndReplies }}</option>
+ <option value="files">{{ $ts.withFiles }}</option>
+ </MkTab>
+ </template>
<XNotes :no-gap="true" :pagination="pagination"/>
-</div>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -33,12 +35,10 @@ const pagination = {
};
</script>
-<style lang="scss" scoped>
-.yrzkoczt {
- > .tab {
- margin: calc(var(--margin) / 2) 0;
- padding: calc(var(--margin) / 2) 0;
- background: var(--bg);
- }
+<style lang="scss" module>
+.tab {
+ margin: calc(var(--margin) / 2) 0;
+ padding: calc(var(--margin) / 2) 0;
+ background: var(--bg);
}
</style>
diff --git a/packages/client/src/scripts/shuffle.ts b/packages/client/src/scripts/shuffle.ts
new file mode 100644
index 0000000000..05e6cdfbcf
--- /dev/null
+++ b/packages/client/src/scripts/shuffle.ts
@@ -0,0 +1,19 @@
+/**
+ * 配列をシャッフル (破壊的)
+ */
+export function shuffle<T extends any[]>(array: T): T {
+ let currentIndex = array.length, randomIndex;
+
+ // While there remain elements to shuffle.
+ while (currentIndex !== 0) {
+ // Pick a remaining element.
+ randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex--;
+
+ // And swap it with the current element.
+ [array[currentIndex], array[randomIndex]] = [
+ array[randomIndex], array[currentIndex]];
+ }
+
+ return array;
+}
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 0f892d2e19..27e33702ad 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -30,7 +30,7 @@ html {
overflow: auto;
overflow-wrap: break-word;
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
- font-size: 15px;
+ font-size: 14px;
line-height: 1.35;
text-size-adjust: 100%;
tab-size: 2;
@@ -61,16 +61,16 @@ html {
}
}
- &.f-small {
- font-size: 0.9em;
+ &.f-1 {
+ font-size: 15px;
}
- &.f-large {
- font-size: 1.1em;
+ &.f-2 {
+ font-size: 16px;
}
- &.f-veryLarge {
- font-size: 1.2em;
+ &.f-3 {
+ font-size: 17px;
}
&.useSystemFont {
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
new file mode 100644
index 0000000000..cae1d25304
--- /dev/null
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -0,0 +1,284 @@
+<template>
+<div class="kmwsukvl">
+ <div class="body">
+ <div class="top">
+ <div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
+ <button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
+ <img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
+ </button>
+ </div>
+ <div class="middle">
+ <MkA v-click-anime class="item index" active-class="active" to="/" exact>
+ <i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
+ </MkA>
+ <template v-for="item in menu">
+ <div v-if="item === '-'" class="divider"></div>
+ <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: navbarItemDef[item].active }]" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
+ <i class="icon fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
+ <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
+ </component>
+ </template>
+ <div class="divider"></div>
+ <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
+ <i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
+ </MkA>
+ <button v-click-anime class="item _button" @click="more">
+ <i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
+ <span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
+ </button>
+ <MkA v-click-anime class="item" active-class="active" to="/settings">
+ <i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
+ </MkA>
+ </div>
+ <div class="bottom">
+ <button class="item _button post" data-cy-open-post-form @click="os.post">
+ <i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
+ </button>
+ <button v-click-anime class="item _button account" @click="openAccountMenu">
+ <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+ </button>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, defineComponent, ref, toRef, watch } from 'vue';
+import { host } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import { navbarItemDef } from '@/navbar';
+import { openAccountMenu as openAccountMenu_ } from '@/account';
+import { defaultStore } from '@/store';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
+
+const menu = toRef(defaultStore.state, 'menu');
+const otherMenuItemIndicated = computed(() => {
+ for (const def in navbarItemDef) {
+ if (menu.value.includes(def)) continue;
+ if (navbarItemDef[def].indicated) return true;
+ }
+ return false;
+});
+
+function openAccountMenu(ev: MouseEvent) {
+ openAccountMenu_({
+ withExtraOperation: true,
+ }, ev);
+}
+
+function openInstanceMenu(ev: MouseEvent) {
+ os.popupMenu([{
+ text: instance.name ?? host,
+ type: 'label',
+ }, {
+ type: 'link',
+ text: i18n.ts.instanceInfo,
+ icon: 'fas fa-info-circle',
+ to: '/about',
+ }, {
+ type: 'link',
+ text: i18n.ts.customEmojis,
+ icon: 'fas fa-laugh',
+ to: '/about#emojis',
+ }, {
+ type: 'link',
+ text: i18n.ts.federation,
+ icon: 'fas fa-globe',
+ to: '/about#federation',
+ }], ev.currentTarget ?? ev.target, {
+ align: 'left',
+ });
+}
+
+function more() {
+ os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {}, {
+ }, 'closed');
+}
+</script>
+
+<style lang="scss" scoped>
+.kmwsukvl {
+ > .body {
+ display: flex;
+ flex-direction: column;
+
+ > .top {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ padding: 20px 0;
+ background: var(--X14);
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(8px));
+
+ > .banner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center center;
+ -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
+ mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
+ }
+
+ > .instance {
+ position: relative;
+ display: block;
+ text-align: center;
+ width: 100%;
+
+ > .icon {
+ display: inline-block;
+ width: 38px;
+ aspect-ratio: 1;
+ }
+ }
+ }
+
+ > .bottom {
+ position: sticky;
+ bottom: 0;
+ padding: 20px 0;
+ background: var(--X14);
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(8px));
+
+ > .post {
+ position: relative;
+ display: block;
+ width: 100%;
+ height: 40px;
+ color: var(--fgOnAccent);
+ font-weight: bold;
+ text-align: left;
+
+ &:before {
+ content: "";
+ display: block;
+ width: calc(100% - 38px);
+ height: 100%;
+ margin: auto;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 999px;
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ }
+
+ &:hover, &.active {
+ &:before {
+ background: var(--accentLighten);
+ }
+ }
+
+ > .icon {
+ position: relative;
+ margin-left: 30px;
+ margin-right: 8px;
+ width: 32px;
+ }
+
+ > .text {
+ position: relative;
+ }
+ }
+
+ > .account {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding-left: 30px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+ text-align: left;
+ box-sizing: border-box;
+ margin-top: 16px;
+
+ > .avatar {
+ position: relative;
+ width: 32px;
+ aspect-ratio: 1;
+ margin-right: 8px;
+ }
+ }
+ }
+
+ > .middle {
+ flex: 1;
+
+ > .divider {
+ margin: 16px 16px;
+ border-top: solid 0.5px var(--divider);
+ }
+
+ > .item {
+ position: relative;
+ display: block;
+ padding-left: 24px;
+ line-height: 2.85rem;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+ text-align: left;
+ box-sizing: border-box;
+ color: var(--navFg);
+
+ > .icon {
+ position: relative;
+ width: 32px;
+ margin-right: 8px;
+ }
+
+ > .indicator {
+ position: absolute;
+ top: 0;
+ left: 20px;
+ color: var(--navIndicator);
+ font-size: 8px;
+ animation: blink 1s infinite;
+ }
+
+ > .text {
+ position: relative;
+ font-size: 0.9em;
+ }
+
+ &:hover {
+ text-decoration: none;
+ color: var(--navHoverFg);
+ }
+
+ &.active {
+ color: var(--navActive);
+ }
+
+ &:hover, &.active {
+ &:before {
+ content: "";
+ display: block;
+ width: calc(100% - 24px);
+ height: 100%;
+ margin: auto;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 999px;
+ background: var(--accentedBg);
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
new file mode 100644
index 0000000000..fbac8425d7
--- /dev/null
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -0,0 +1,492 @@
+<template>
+<div class="mvcprjjd" :class="{ iconOnly }">
+ <div class="body">
+ <div class="top">
+ <div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
+ <button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
+ <img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
+ </button>
+ </div>
+ <div class="middle">
+ <MkA v-click-anime v-tooltip.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact>
+ <i class="icon fas fa-home fa-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
+ </MkA>
+ <template v-for="item in menu">
+ <div v-if="item === '-'" class="divider"></div>
+ <component
+ :is="navbarItemDef[item].to ? 'MkA' : 'button'"
+ v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
+ v-click-anime
+ v-tooltip.right="i18n.ts[navbarItemDef[item].title]"
+ class="item _button"
+ :class="[item, { active: navbarItemDef[item].active }]"
+ active-class="active"
+ :to="navbarItemDef[item].to"
+ v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
+ >
+ <i class="icon fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ i18n.ts[navbarItemDef[item].title] }}</span>
+ <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
+ </component>
+ </template>
+ <div class="divider"></div>
+ <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin">
+ <i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
+ </MkA>
+ <button v-click-anime class="item _button" @click="more">
+ <i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ i18n.ts.more }}</span>
+ <span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
+ </button>
+ <MkA v-click-anime v-tooltip.right="i18n.ts.settings" class="item" active-class="active" to="/settings">
+ <i class="icon fas fa-cog fa-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
+ </MkA>
+ </div>
+ <div class="bottom">
+ <button v-tooltip.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post">
+ <i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ i18n.ts.note }}</span>
+ </button>
+ <button v-click-anime v-tooltip.right="i18n.ts.account" class="item _button account" @click="openAccountMenu">
+ <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+ </button>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import * as os from '@/os';
+import { navbarItemDef } from '@/navbar';
+import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { host } from '@/config';
+
+const iconOnly = ref(false);
+
+const menu = computed(() => defaultStore.state.menu);
+const otherMenuItemIndicated = computed(() => {
+ for (const def in navbarItemDef) {
+ if (menu.value.includes(def)) continue;
+ if (navbarItemDef[def].indicated) return true;
+ }
+ return false;
+});
+
+const calcViewState = () => {
+ iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
+};
+
+calcViewState();
+
+window.addEventListener('resize', calcViewState);
+
+watch(defaultStore.reactiveState.menuDisplay, () => {
+ calcViewState();
+});
+
+function openAccountMenu(ev: MouseEvent) {
+ openAccountMenu_({
+ withExtraOperation: true,
+ }, ev);
+}
+
+function openInstanceMenu(ev: MouseEvent) {
+ os.popupMenu([{
+ text: instance.name ?? host,
+ type: 'label',
+ }, {
+ type: 'link',
+ text: i18n.ts.instanceInfo,
+ icon: 'fas fa-info-circle',
+ to: '/about',
+ }, {
+ type: 'link',
+ text: i18n.ts.customEmojis,
+ icon: 'fas fa-laugh',
+ to: '/about#emojis',
+ }, {
+ type: 'link',
+ text: i18n.ts.federation,
+ icon: 'fas fa-globe',
+ to: '/about#federation',
+ }], ev.currentTarget ?? ev.target, {
+ align: 'left',
+ });
+}
+
+function more(ev: MouseEvent) {
+ os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {
+ src: ev.currentTarget ?? ev.target,
+ }, {
+ }, 'closed');
+}
+</script>
+
+<style lang="scss" scoped>
+.mvcprjjd {
+ $nav-width: 250px;
+ $nav-icon-only-width: 86px;
+
+ flex: 0 0 $nav-width;
+ width: $nav-width;
+ box-sizing: border-box;
+
+ > .body {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1001;
+ width: $nav-icon-only-width;
+ // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+ height: calc(var(--vh, 1vh) * 100);
+ box-sizing: border-box;
+ overflow: auto;
+ overflow-x: clip;
+ background: var(--navBg);
+ contain: strict;
+ display: flex;
+ flex-direction: column;
+ }
+
+ &:not(.iconOnly) {
+ > .body {
+ width: $nav-width;
+
+ > .top {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ padding: 20px 0;
+ background: var(--X14);
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(8px));
+
+ > .banner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center center;
+ -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
+ mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
+ }
+
+ > .instance {
+ position: relative;
+ display: block;
+ text-align: center;
+ width: 100%;
+
+ > .icon {
+ display: inline-block;
+ width: 38px;
+ aspect-ratio: 1;
+ }
+ }
+ }
+
+ > .bottom {
+ position: sticky;
+ bottom: 0;
+ padding: 20px 0;
+ background: var(--X14);
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(8px));
+
+ > .post {
+ position: relative;
+ display: block;
+ width: 100%;
+ height: 40px;
+ color: var(--fgOnAccent);
+ font-weight: bold;
+ text-align: left;
+
+ &:before {
+ content: "";
+ display: block;
+ width: calc(100% - 38px);
+ height: 100%;
+ margin: auto;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 999px;
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ }
+
+ &:hover, &.active {
+ &:before {
+ background: var(--accentLighten);
+ }
+ }
+
+ > .icon {
+ position: relative;
+ margin-left: 30px;
+ margin-right: 8px;
+ width: 32px;
+ }
+
+ > .text {
+ position: relative;
+ }
+ }
+
+ > .account {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding-left: 30px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+ text-align: left;
+ box-sizing: border-box;
+ margin-top: 16px;
+
+ > .avatar {
+ position: relative;
+ width: 32px;
+ aspect-ratio: 1;
+ margin-right: 8px;
+ }
+ }
+ }
+
+ > .middle {
+ flex: 1;
+
+ > .divider {
+ margin: 16px 16px;
+ border-top: solid 0.5px var(--divider);
+ }
+
+ > .item {
+ position: relative;
+ display: block;
+ padding-left: 30px;
+ line-height: 2.85rem;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+ text-align: left;
+ box-sizing: border-box;
+ color: var(--navFg);
+
+ > .icon {
+ position: relative;
+ width: 32px;
+ margin-right: 8px;
+ }
+
+ > .indicator {
+ position: absolute;
+ top: 0;
+ left: 20px;
+ color: var(--navIndicator);
+ font-size: 8px;
+ animation: blink 1s infinite;
+ }
+
+ > .text {
+ position: relative;
+ font-size: 0.9em;
+ }
+
+ &:hover {
+ text-decoration: none;
+ color: var(--navHoverFg);
+ }
+
+ &.active {
+ color: var(--navActive);
+ }
+
+ &:hover, &.active {
+ color: var(--accent);
+
+ &:before {
+ content: "";
+ display: block;
+ width: calc(100% - 34px);
+ height: 100%;
+ margin: auto;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 999px;
+ background: var(--accentedBg);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ &.iconOnly {
+ flex: 0 0 $nav-icon-only-width;
+ width: $nav-icon-only-width;
+
+ > .body {
+ width: $nav-icon-only-width;
+
+ > .top {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ padding: 20px 0;
+ background: var(--X14);
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(8px));
+
+ > .instance {
+ display: block;
+ text-align: center;
+ width: 100%;
+
+ > .icon {
+ display: inline-block;
+ width: 38px;
+ aspect-ratio: 1;
+ }
+ }
+ }
+
+ > .bottom {
+ position: sticky;
+ bottom: 0;
+ padding: 20px 0;
+ background: var(--X14);
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(8px));
+
+ > .post {
+ display: block;
+ position: relative;
+ width: 100%;
+ height: 52px;
+ margin-bottom: 16px;
+ text-align: center;
+
+ &:before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: auto;
+ width: 52px;
+ aspect-ratio: 1/1;
+ border-radius: 100%;
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ }
+
+ &:hover, &.active {
+ &:before {
+ background: var(--accentLighten);
+ }
+ }
+
+ > .icon {
+ position: relative;
+ color: var(--fgOnAccent);
+ }
+
+ > .text {
+ display: none;
+ }
+ }
+
+ > .account {
+ display: block;
+ text-align: center;
+ width: 100%;
+
+ > .avatar {
+ display: inline-block;
+ width: 38px;
+ aspect-ratio: 1;
+ }
+
+ > .text {
+ display: none;
+ }
+ }
+ }
+
+ > .middle {
+ flex: 1;
+
+ > .divider {
+ margin: 8px auto;
+ width: calc(100% - 32px);
+ border-top: solid 0.5px var(--divider);
+ }
+
+ > .item {
+ display: block;
+ position: relative;
+ padding: 18px 0;
+ width: 100%;
+ text-align: center;
+
+ > .icon {
+ display: block;
+ margin: 0 auto;
+ opacity: 0.7;
+ }
+
+ > .text {
+ display: none;
+ }
+
+ > .indicator {
+ position: absolute;
+ top: 6px;
+ left: 24px;
+ color: var(--navIndicator);
+ font-size: 8px;
+ animation: blink 1s infinite;
+ }
+
+ &:hover, &.active {
+ text-decoration: none;
+ color: var(--accent);
+
+ &:before {
+ content: "";
+ display: block;
+ height: 100%;
+ aspect-ratio: 1;
+ margin: auto;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 999px;
+ background: var(--accentedBg);
+ }
+
+ > .icon, > .text {
+ opacity: 1;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/ui/_common_/sidebar-for-mobile.vue b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
deleted file mode 100644
index e789ae5e06..0000000000
--- a/packages/client/src/ui/_common_/sidebar-for-mobile.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<template>
-<div class="kmwsukvl">
- <div class="body">
- <button v-click-anime class="item _button account" @click="openAccountMenu">
- <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
- </button>
- <MkA v-click-anime class="item index" active-class="active" to="/" exact>
- <i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
- </MkA>
- <template v-for="item in menu">
- <div v-if="item === '-'" class="divider"></div>
- <component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
- <i class="icon fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
- <span v-if="menuDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
- </component>
- </template>
- <div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
- <i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
- </MkA>
- <button v-click-anime class="item _button" @click="more">
- <i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
- <span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
- </button>
- <MkA v-click-anime class="item" active-class="active" to="/settings">
- <i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
- </MkA>
- <button class="item _button post" data-cy-open-post-form @click="post">
- <i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
- </button>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, ref, toRef, watch } from 'vue';
-import { host } from '@/config';
-import { search } from '@/scripts/search';
-import * as os from '@/os';
-import { menuDef } from '@/menu';
-import { openAccountMenu } from '@/account';
-import { defaultStore } from '@/store';
-
-export default defineComponent({
- setup(props, context) {
- const menu = toRef(defaultStore.state, 'menu');
- const otherMenuItemIndicated = computed(() => {
- for (const def in menuDef) {
- if (menu.value.includes(def)) continue;
- if (menuDef[def].indicated) return true;
- }
- return false;
- });
-
- return {
- host: host,
- accounts: [],
- connection: null,
- menu,
- menuDef: menuDef,
- otherMenuItemIndicated,
- post: os.post,
- search,
- openAccountMenu: (ev) => {
- openAccountMenu({
- withExtraOperation: true,
- }, ev);
- },
- more: () => {
- os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {}, {
- }, 'closed');
- },
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.kmwsukvl {
- $ui-font-size: 1em; // TODO: どこかに集約したい
- $avatar-size: 32px;
- $avatar-margin: 8px;
-
- > .body {
-
- > .divider {
- margin: 16px 16px;
- border-top: solid 0.5px var(--divider);
- }
-
- > .item {
- position: relative;
- display: block;
- padding-left: 24px;
- font-size: $ui-font-size;
- line-height: 2.85rem;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- width: 100%;
- text-align: left;
- box-sizing: border-box;
- color: var(--navFg);
-
- > .icon {
- position: relative;
- width: 32px;
- }
-
- > .icon,
- > .avatar {
- margin-right: $avatar-margin;
- }
-
- > .avatar {
- width: $avatar-size;
- height: $avatar-size;
- vertical-align: middle;
- }
-
- > .indicator {
- position: absolute;
- top: 0;
- left: 20px;
- color: var(--navIndicator);
- font-size: 8px;
- animation: blink 1s infinite;
- }
-
- > .text {
- position: relative;
- font-size: 0.9em;
- }
-
- &:hover {
- text-decoration: none;
- color: var(--navHoverFg);
- }
-
- &.active {
- color: var(--navActive);
- }
-
- &:hover, &.active {
- &:before {
- content: "";
- display: block;
- width: calc(100% - 24px);
- height: 100%;
- margin: auto;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- border-radius: 999px;
- background: var(--accentedBg);
- }
- }
-
- &:first-child, &:last-child {
- position: sticky;
- z-index: 1;
- padding-top: 8px;
- padding-bottom: 8px;
- background: var(--X14);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
- }
-
- &:first-child {
- top: 0;
-
- &:hover, &.active {
- &:before {
- content: none;
- }
- }
- }
-
- &:last-child {
- bottom: 0;
- color: var(--fgOnAccent);
-
- &:before {
- content: "";
- display: block;
- width: calc(100% - 20px);
- height: calc(100% - 20px);
- margin: auto;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- border-radius: 999px;
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
- }
-
- &:hover, &.active {
- &:before {
- background: var(--accentLighten);
- }
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue
deleted file mode 100644
index a72bf786ad..0000000000
--- a/packages/client/src/ui/_common_/sidebar.vue
+++ /dev/null
@@ -1,303 +0,0 @@
-<template>
-<div class="mvcprjjd" :class="{ iconOnly }">
- <div class="body">
- <button v-click-anime class="item _button account" @click="openAccountMenu">
- <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
- </button>
- <MkA v-click-anime class="item index" active-class="active" to="/" exact>
- <i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
- </MkA>
- <template v-for="item in menu">
- <div v-if="item === '-'" class="divider"></div>
- <component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
- <i class="icon fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
- <span v-if="menuDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
- </component>
- </template>
- <div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
- <i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
- </MkA>
- <button v-click-anime class="item _button" @click="more">
- <i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
- <span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
- </button>
- <MkA v-click-anime class="item" active-class="active" to="/settings">
- <i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
- </MkA>
- <button class="item _button post" data-cy-open-post-form @click="os.post">
- <i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
- </button>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, watch } from 'vue';
-import * as os from '@/os';
-import { menuDef } from '@/menu';
-import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
-import { defaultStore } from '@/store';
-
-const iconOnly = ref(false);
-
-const menu = computed(() => defaultStore.state.menu);
-const otherMenuItemIndicated = computed(() => {
- for (const def in menuDef) {
- if (menu.value.includes(def)) continue;
- if (menuDef[def].indicated) return true;
- }
- return false;
-});
-
-const calcViewState = () => {
- iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
-};
-
-calcViewState();
-
-window.addEventListener('resize', calcViewState);
-
-watch(defaultStore.reactiveState.menuDisplay, () => {
- calcViewState();
-});
-
-function openAccountMenu(ev: MouseEvent) {
- openAccountMenu_({
- withExtraOperation: true,
- }, ev);
-}
-
-function more(ev: MouseEvent) {
- os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {
- src: ev.currentTarget ?? ev.target,
- }, {
- }, 'closed');
-}
-</script>
-
-<style lang="scss" scoped>
-.mvcprjjd {
- $ui-font-size: 1em; // TODO: どこかに集約したい
- $nav-width: 250px;
- $nav-icon-only-width: 86px;
- $avatar-size: 32px;
- $avatar-margin: 8px;
-
- flex: 0 0 $nav-width;
- width: $nav-width;
- box-sizing: border-box;
-
- > .body {
- position: fixed;
- top: 0;
- left: 0;
- z-index: 1001;
- width: $nav-width;
- // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
- height: calc(var(--vh, 1vh) * 100);
- box-sizing: border-box;
- overflow: auto;
- overflow-x: clip;
- background: var(--navBg);
- contain: strict;
-
- > .divider {
- margin: 16px 16px;
- border-top: solid 0.5px var(--divider);
- }
-
- > .item {
- position: relative;
- display: block;
- padding-left: 24px;
- font-size: $ui-font-size;
- line-height: 2.85rem;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- width: 100%;
- text-align: left;
- box-sizing: border-box;
- color: var(--navFg);
-
- > .icon {
- position: relative;
- width: 32px;
- }
-
- > .icon,
- > .avatar {
- margin-right: $avatar-margin;
- }
-
- > .avatar {
- width: $avatar-size;
- height: $avatar-size;
- vertical-align: middle;
- }
-
- > .indicator {
- position: absolute;
- top: 0;
- left: 20px;
- color: var(--navIndicator);
- font-size: 8px;
- animation: blink 1s infinite;
- }
-
- > .text {
- position: relative;
- font-size: 0.9em;
- }
-
- &:hover {
- text-decoration: none;
- color: var(--navHoverFg);
- }
-
- &.active {
- color: var(--navActive);
- }
-
- &:hover, &.active {
- color: var(--accent);
-
- &:before {
- content: "";
- display: block;
- width: calc(100% - 24px);
- height: 100%;
- margin: auto;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- border-radius: 999px;
- background: var(--accentedBg);
- }
- }
-
- &:first-child, &:last-child {
- position: sticky;
- z-index: 1;
- padding-top: 8px;
- padding-bottom: 8px;
- background: var(--X14);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
- }
-
- &:first-child {
- top: 0;
-
- &:hover, &.active {
- &:before {
- content: none;
- }
- }
- }
-
- &:last-child {
- bottom: 0;
- color: var(--fgOnAccent);
-
- &:before {
- content: "";
- display: block;
- width: calc(100% - 20px);
- height: calc(100% - 20px);
- margin: auto;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- border-radius: 999px;
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
- }
-
- &:hover, &.active {
- &:before {
- background: var(--accentLighten);
- }
- }
- }
- }
- }
-
- &.iconOnly {
- flex: 0 0 $nav-icon-only-width;
- width: $nav-icon-only-width;
-
- > .body {
- width: $nav-icon-only-width;
-
- > .divider {
- margin: 8px auto;
- width: calc(100% - 32px);
- }
-
- > .item {
- padding-left: 0;
- padding: 18px 0;
- width: 100%;
- text-align: center;
- font-size: $ui-font-size * 1.1;
- line-height: initial;
-
- > .icon,
- > .avatar {
- display: block;
- margin: 0 auto;
- }
-
- > .icon {
- opacity: 0.7;
- }
-
- > .text {
- display: none;
- }
-
- &:hover, &.active {
- > .icon, > .text {
- opacity: 1;
- }
- }
-
- &:first-child {
- margin-bottom: 8px;
- }
-
- &:last-child {
- margin-top: 8px;
- }
-
- &:before {
- width: min-content;
- height: 100%;
- aspect-ratio: 1/1;
- border-radius: 8px;
- }
-
- &.post {
- height: $nav-icon-only-width;
-
- > .icon {
- opacity: 1;
- }
- }
-
- &.post:before {
- width: calc(100% - 28px);
- height: auto;
- aspect-ratio: 1/1;
- border-radius: 100%;
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/_common_/statusbar-rss.vue b/packages/client/src/ui/_common_/statusbar-rss.vue
index 88604a38a7..635b875ca1 100644
--- a/packages/client/src/ui/_common_/statusbar-rss.vue
+++ b/packages/client/src/ui/_common_/statusbar-rss.vue
@@ -20,9 +20,11 @@ import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
import MarqueeText from '@/components/marquee.vue';
import * as os from '@/os';
import { useInterval } from '@/scripts/use-interval';
+import { shuffle } from '@/scripts/shuffle';
const props = defineProps<{
url?: string;
+ shuffle?: boolean;
display?: 'marquee' | 'oneByOne';
marqueeDuration?: number;
marqueeReverse?: boolean;
@@ -37,6 +39,9 @@ let key = $ref(0);
const tick = () => {
fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
res.json().then(feed => {
+ if (props.shuffle) {
+ shuffle(feed.items);
+ }
items.value = feed.items;
fetching.value = false;
key++;
diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue
index e50b4e54c3..114ca5be8c 100644
--- a/packages/client/src/ui/_common_/statusbars.vue
+++ b/packages/client/src/ui/_common_/statusbars.vue
@@ -10,7 +10,7 @@
}]"
>
<span class="name">{{ x.name }}</span>
- <XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url"/>
+ <XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/>
<XFederation v-else-if="x.type === 'federation'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
<XUserList v-else-if="x.type === 'userList'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :user-list-id="x.props.userListId"/>
</div>
@@ -28,6 +28,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
<style lang="scss" scoped>
.dlrsnxqu {
+ font-size: 15px;
background: var(--panel);
> .item {
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 57008aeaed..131767c0e3 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -7,9 +7,9 @@
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
- <component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime v-tooltip="$ts[menuDef[item].title]" class="item _button" :class="item" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
- <i class="fa-fw" :class="menuDef[item].icon"></i>
- <span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+ <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="$ts[navbarItemDef[item].title]" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
+ <i class="fa-fw" :class="navbarItemDef[item].icon"></i>
+ <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
@@ -43,7 +43,7 @@ import { defineAsyncComponent, defineComponent } from 'vue';
import { host } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
-import { menuDef } from '@/menu';
+import { navbarItemDef } from '@/navbar';
import { openAccountMenu } from '@/account';
import MkButton from '@/components/ui/button.vue';
@@ -57,7 +57,7 @@ export default defineComponent({
host: host,
accounts: [],
connection: null,
- menuDef: menuDef,
+ navbarItemDef: navbarItemDef,
settingsWindowed: false,
};
},
@@ -68,9 +68,9 @@ export default defineComponent({
},
otherNavItemIndicated(): boolean {
- for (const def in this.menuDef) {
+ for (const def in this.navbarItemDef) {
if (this.menu.includes(def)) continue;
- if (this.menuDef[def].indicated) return true;
+ if (this.navbarItemDef[def].indicated) return true;
}
return false;
},
@@ -113,7 +113,7 @@ export default defineComponent({
withExtraOperation: true,
}, ev);
},
- }
+ },
});
</script>
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index 6c0ce023e4..172401f420 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -14,9 +14,9 @@
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
- <component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="item" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
- <i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
- <span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+ <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
+ <i class="fa-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
+ <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
@@ -45,7 +45,7 @@ import { defineAsyncComponent, defineComponent } from 'vue';
import { host } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
-import { menuDef } from '@/menu';
+import { navbarItemDef } from '@/navbar';
import { openAccountMenu } from '@/account';
import MkButton from '@/components/ui/button.vue';
import { StickySidebar } from '@/scripts/sticky-sidebar';
@@ -62,7 +62,7 @@ export default defineComponent({
host: host,
accounts: [],
connection: null,
- menuDef: menuDef,
+ navbarItemDef: navbarItemDef,
iconOnly: false,
settingsWindowed: false,
};
@@ -74,9 +74,9 @@ export default defineComponent({
},
otherNavItemIndicated(): boolean {
- for (const def in this.menuDef) {
+ for (const def in this.navbarItemDef) {
if (this.menu.includes(def)) continue;
- if (this.menuDef[def].indicated) return true;
+ if (this.navbarItemDef[def].indicated) return true;
}
return false;
},
@@ -131,7 +131,7 @@ export default defineComponent({
withExtraOperation: true,
}, ev);
},
- }
+ },
});
</script>
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 70db7ed12b..c42407f5b0 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -47,7 +47,6 @@ import XCommon from './_common_/common.vue';
import { instanceName } from '@/config';
import { StickySidebar } from '@/scripts/sticky-sidebar';
import * as os from '@/os';
-import { menuDef } from '@/menu';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
import { defaultStore } from '@/store';
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 19a99a95aa..94fee1424e 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -69,12 +69,12 @@ import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
import DeckColumnCore from '@/ui/deck/column-core.vue';
-import XSidebar from '@/ui/_common_/sidebar.vue';
-import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
+import XSidebar from '@/ui/_common_/navbar.vue';
+import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import MkButton from '@/components/ui/button.vue';
import { getScrollContainer } from '@/scripts/scroll';
import * as os from '@/os';
-import { menuDef } from '@/menu';
+import { navbarItemDef } from '@/navbar';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { mainRouter } from '@/router';
@@ -105,8 +105,8 @@ const columns = deckStore.reactiveState.columns;
const layout = deckStore.reactiveState.layout;
const menuIndicated = computed(() => {
if ($i == null) return false;
- for (const def in menuDef) {
- if (menuDef[def].indicated) return true;
+ for (const def in navbarItemDef) {
+ if (navbarItemDef[def].indicated) return true;
}
return false;
});
@@ -359,9 +359,10 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
height: calc(var(--vh, 1vh) * 100);
width: 240px;
box-sizing: border-box;
+ contain: strict;
overflow: auto;
overscroll-behavior: contain;
- background: var(--bg);
+ background: var(--navBg);
}
}
</style>
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
index e8e554d72b..4d34ca9b8e 100644
--- a/packages/client/src/ui/deck/column.vue
+++ b/packages/client/src/ui/deck/column.vue
@@ -23,7 +23,7 @@
<slot name="action"></slot>
</div>
<span class="header"><slot name="header"></slot></span>
- <button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-cog"></i></button>
+ <button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-ellipsis"></i></button>
</header>
<div v-show="active" ref="body">
<slot></slot>
@@ -361,7 +361,6 @@ function onDrop(ev) {
z-index: 1;
width: var(--deckColumnHeaderHeight);
line-height: var(--deckColumnHeaderHeight);
- font-size: 16px;
color: var(--faceTextButton);
&:hover {
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 2edfb3f12d..e4b5de9918 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -61,17 +61,17 @@ import { defineAsyncComponent, provide, onMounted, computed, ref, watch, Compute
import XCommon from './_common_/common.vue';
import { instanceName } from '@/config';
import { StickySidebar } from '@/scripts/sticky-sidebar';
-import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
+import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import * as os from '@/os';
import { defaultStore } from '@/store';
-import { menuDef } from '@/menu';
+import { navbarItemDef } from '@/navbar';
import { i18n } from '@/i18n';
import { $i } from '@/account';
import { Router } from '@/nirax';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
-const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/sidebar.vue'));
+const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const DESKTOP_THRESHOLD = 1100;
@@ -97,9 +97,9 @@ provideMetadataReceiver((info) => {
});
const menuIndicated = computed(() => {
- for (const def in menuDef) {
+ for (const def in navbarItemDef) {
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
- if (menuDef[def].indicated) return true;
+ if (navbarItemDef[def].indicated) return true;
}
return false;
});
@@ -365,11 +365,11 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
height: calc(var(--vh, 1vh) * 100);
width: 240px;
box-sizing: border-box;
+ contain: strict;
overflow: auto;
overscroll-behavior: contain;
- background: var(--bg);
+ background: var(--navBg);
}
-
}
</style>
diff --git a/packages/client/src/widgets/rss-ticker.vue b/packages/client/src/widgets/rss-ticker.vue
index 06995bc865..c692c0c4ff 100644
--- a/packages/client/src/widgets/rss-ticker.vue
+++ b/packages/client/src/widgets/rss-ticker.vue
@@ -26,6 +26,7 @@ import { GetFormResultType } from '@/scripts/form';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
import { useInterval } from '@/scripts/use-interval';
+import { shuffle } from '@/scripts/shuffle';
const name = 'rssTicker';
@@ -34,6 +35,10 @@ const widgetPropsDef = {
type: 'string' as const,
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
},
+ shuffle: {
+ type: 'boolean' as const,
+ default: true,
+ },
refreshIntervalSec: {
type: 'number' as const,
default: 60,
@@ -80,6 +85,9 @@ let key = $ref(0);
const tick = () => {
fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
res.json().then(feed => {
+ if (widgetProps.shuffle) {
+ shuffle(feed.items);
+ }
items.value = feed.items;
fetching.value = false;
key++;
diff --git a/packages/client/vite.json5.ts b/packages/client/vite.json5.ts
index 693ee7be06..0a37fbff44 100644
--- a/packages/client/vite.json5.ts
+++ b/packages/client/vite.json5.ts
@@ -6,33 +6,33 @@ import { createFilter, dataToEsm } from '@rollup/pluginutils';
import { RollupJsonOptions } from '@rollup/plugin-json';
export default function json5(options: RollupJsonOptions = {}): Plugin {
- const filter = createFilter(options.include, options.exclude);
- const indent = 'indent' in options ? options.indent : '\t';
+ const filter = createFilter(options.include, options.exclude);
+ const indent = 'indent' in options ? options.indent : '\t';
- return {
- name: 'json5',
+ return {
+ name: 'json5',
- // eslint-disable-next-line no-shadow
- transform(json, id) {
- if (id.slice(-6) !== '.json5' || !filter(id)) return null;
+ // eslint-disable-next-line no-shadow
+ transform(json, id) {
+ if (id.slice(-6) !== '.json5' || !filter(id)) return null;
- try {
- const parsed = JSON5.parse(json);
- return {
- code: dataToEsm(parsed, {
- preferConst: options.preferConst,
- compact: options.compact,
- namedExports: options.namedExports,
- indent
- }),
- map: { mappings: '' }
- };
- } catch (err) {
- const message = 'Could not parse JSON file';
- const position = parseInt(/[\d]/.exec(err.message)[0], 10);
- this.warn({ message, id, position });
- return null;
- }
- }
- };
+ try {
+ const parsed = JSON5.parse(json);
+ return {
+ code: dataToEsm(parsed, {
+ preferConst: options.preferConst,
+ compact: options.compact,
+ namedExports: options.namedExports,
+ indent,
+ }),
+ map: { mappings: '' },
+ };
+ } catch (err) {
+ const message = 'Could not parse JSON file';
+ const position = parseInt(/[\d]/.exec(err.message)[0], 10);
+ this.warn({ message, id, position });
+ return null;
+ }
+ },
+ };
}