summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkImgPreviewDialog.vue2
-rw-r--r--packages/frontend/src/components/MkPostForm.vue16
-rw-r--r--packages/frontend/src/components/MkPostFormDialog.vue4
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue19
-rw-r--r--packages/frontend/src/pages/admin/performance.vue2
-rw-r--r--packages/frontend/src/pages/flash/flash.vue4
-rw-r--r--packages/frontend/src/pages/settings/index.vue6
-rw-r--r--packages/frontend/src/pages/settings/other.vue4
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue4
-rw-r--r--packages/frontend/src/pages/settings/profile.vue2
-rw-r--r--packages/frontend/src/pages/settings/security.vue2
-rw-r--r--packages/frontend/src/preferences/manager.ts21
-rw-r--r--packages/frontend/src/store.ts4
-rw-r--r--packages/frontend/src/tips.ts1
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue11
-rw-r--r--packages/frontend/src/ui/deck.vue52
-rw-r--r--packages/frontend/src/ui/universal.vue15
-rw-r--r--packages/frontend/src/utility/storage.ts34
18 files changed, 145 insertions, 58 deletions
diff --git a/packages/frontend/src/components/MkImgPreviewDialog.vue b/packages/frontend/src/components/MkImgPreviewDialog.vue
index 3e6e4e0ec9..e17a1651cf 100644
--- a/packages/frontend/src/components/MkImgPreviewDialog.vue
+++ b/packages/frontend/src/components/MkImgPreviewDialog.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkModalWindow>
</template>
<script lang="ts" setup>
-import { defineProps, ref } from 'vue';
+import { ref } from 'vue';
import MkModalWindow from './MkModalWindow.vue';
import type * as Misskey from 'misskey-js';
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 86557b12df..b3bcfcc137 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- :class="[$style.root, { [$style.modal]: modal, _popup: modal }]"
+ :class="[$style.root]"
@dragover.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
+import { watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@@ -161,8 +161,6 @@ import { closeTip } from '@/tips.js';
const $i = ensureSignin();
-const modal = inject(DI.inModal, false);
-
const props = withDefaults(defineProps<PostFormProps & {
fixed?: boolean;
autofocus?: boolean;
@@ -1447,13 +1445,6 @@ defineExpose({
.root {
position: relative;
container-type: inline-size;
-
- &.modal {
- width: 100%;
- max-width: 520px;
- overflow-x: clip;
- overflow-y: auto;
- }
}
//#region header
@@ -1722,7 +1713,8 @@ html[data-color-scheme=light] .preview {
min-width: 100%;
width: 100%;
min-height: 90px;
- height: 100%;
+ max-height: 500px;
+ field-sizing: content;
}
.textCount {
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index ba8d3a7210..a7cf8a37cf 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPostForm
ref="form"
:class="$style.form"
+ class="_popup"
v-bind="props"
autofocus
freezeAfterPosted
@@ -73,7 +74,8 @@ function onModalClosed() {
<style lang="scss" module>
.form {
- max-height: 100%;
+ width: 100%;
+ max-width: 520px;
margin: 0 auto auto auto;
}
</style>
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index c2548cc7be..e7208ed574 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="showDecoration">
<img
v-for="decoration in decorations ?? user.avatarDecorations"
- :class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]"
+ :class="[$style.decoration, { [$style.decorationBlink]: getDecorationIsBrink(decoration) }]"
:src="getDecorationUrl(decoration)"
:style="{
rotate: getDecorationAngle(decoration),
@@ -56,13 +56,16 @@ import { prefer } from '@/preferences.js';
const animation = ref(prefer.s.animation);
const squareAvatars = ref(prefer.s.squareAvatars);
+type Decoration = Misskey.entities.UserDetailed['avatarDecorations'][number];
+type DecorationEditorDecoration = Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; };
+
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
target?: string | null;
link?: boolean;
preview?: boolean;
indicator?: boolean;
- decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[];
+ decorations?: DecorationEditorDecoration[];
forceShowDecoration?: boolean;
}>(), {
target: null,
@@ -93,27 +96,31 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
-function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationUrl(decoration: Decoration | DecorationEditorDecoration) {
if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(decoration.url);
return decoration.url;
}
-function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationAngle(decoration: Decoration | DecorationEditorDecoration) {
const angle = decoration.angle ?? 0;
return angle === 0 ? undefined : `${angle * 360}deg`;
}
-function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationScale(decoration: Decoration | DecorationEditorDecoration) {
const scaleX = decoration.flipH ? -1 : 1;
return scaleX === 1 ? undefined : `${scaleX} 1`;
}
-function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationOffset(decoration: Decoration | DecorationEditorDecoration) {
const offsetX = decoration.offsetX ?? 0;
const offsetY = decoration.offsetY ?? 0;
return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
}
+function getDecorationIsBrink(decoration: Decoration | DecorationEditorDecoration) {
+ return 'blink' in decoration && decoration.blink === true;
+}
+
const color = ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index c5f3c2d4f0..3ce8e05982 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -122,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
- <template #label><SearchLabel>Misskey® Reactions Boost Technology™ (RBT)</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #label><SearchLabel>Misskey® Reactions Boost Technology™ (RBT)</SearchLabel></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 2913aaae64..efc9ee014f 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -193,7 +193,7 @@ function start() {
}
function getIsLegacy(version: string | null): boolean {
- if (version == null) return false;
+ if (version == null) return true;
try {
return compareVersions(version, '1.0.0') < 0;
} catch {
@@ -206,7 +206,7 @@ async function run() {
if (!flash.value) return;
const version = utils.getLangVersion(flash.value.script);
- const isLegacy = version != null && getIsLegacy(version);
+ const isLegacy = getIsLegacy(version);
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 250c1735be..39c32d347f 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -11,6 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
<div class="_gaps_s">
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="!storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
+ <div>{{ i18n.ts._settings.settingsPersistence_description1 }}</div>
+ <div>{{ i18n.ts._settings.settingsPersistence_description2 }}</div>
+ <div><button class="_textButton" @click="enableStoragePersistence">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipStoragePersistence">{{ i18n.ts.skip }}</button></div>
+ </MkInfo>
<MkInfo v-if="!store.r.enablePreferencesAutoCloudBackup.value && store.r.showPreferencesAutoCloudBackupSuggestion.value" class="info">
<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
@@ -46,6 +51,7 @@ import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utili
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
+import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index e6ee3bfb1c..d4097bde94 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -142,6 +142,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr>
</template>
+ <MkButton v-if="!storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
+
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
<FormSlot>
@@ -163,7 +165,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
+import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index c2e0b3fe41..edc71e5156 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['lockdown']">
<FormSection>
- <template #label><SearchLabel>{{ i18n.ts.lockdown }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #label><SearchLabel>{{ i18n.ts.lockdown }}</SearchLabel></template>
<div class="_gaps_m">
<SearchMarker :keywords="['login', 'signin']">
@@ -213,9 +213,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
-import type { MkSelectItem } from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 89325dee63..7d3da470d6 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -107,7 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['follow', 'message']">
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
- <template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel></template>
<template #caption>
<div><SearchText>{{ i18n.ts._profile.followedMessageDescription }}</SearchText></div>
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index bc77c1f0af..30f97b095e 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['signin', 'login', 'history', 'log']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.signinHistory }}</SearchLabel></template>
- <MkPagination :paginator="paginator" withControl>
+ <MkPagination :paginator="paginator" withControl :forceDisableInfiniteScroll="true">
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index 5949ee71eb..13ba0000e4 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -257,20 +257,23 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
this.rewriteRawState(key, v);
- this.emit('committed', {
- key,
- value: v,
- oldValue: this.s[key],
- });
-
const record = this.getMatchedRecordOf(key);
+ const _save = () => {
+ this.save();
+ this.emit('committed', {
+ key,
+ value: v,
+ oldValue: this.s[key],
+ });
+ };
+
if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) {
this.profile.preferences[key].push([makeScope({
server: host,
account: currentAccount.id,
}), v, {}]);
- this.save();
+ _save();
return;
}
@@ -278,12 +281,12 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
this.profile.preferences[key].push([makeScope({
server: host,
}), v, {}]);
- this.save();
+ _save();
return;
}
record[1] = v;
- this.save();
+ _save();
if (record[2].sync) {
// awaitの必要なし
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 073fbba0fb..fb9349c42f 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -118,6 +118,10 @@ export const store = markRaw(new Pizzax('base', {
where: 'device',
default: true,
},
+ showStoragePersistenceSuggestion: {
+ where: 'device',
+ default: true,
+ },
//#region TODO: そのうち消す (preferに移行済み)
defaultWithReplies: {
diff --git a/packages/frontend/src/tips.ts b/packages/frontend/src/tips.ts
index 8a58e2aa63..6ee7130ee9 100644
--- a/packages/frontend/src/tips.ts
+++ b/packages/frontend/src/tips.ts
@@ -12,6 +12,7 @@ export const TIPS = [
'clips',
'userLists',
'postForm',
+ 'deck',
'tl.home',
'tl.local',
'tl.social',
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index f37e7ae85e..c679ee7a92 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -14,6 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-if="store.r.realtimeMode.value" class="ti ti-bolt ti-fw"></i>
<i v-else class="ti ti-bolt-off ti-fw"></i>
</button>
+ <button v-if="!iconOnly && showWidgetButton" v-tooltip.noDelay.right="i18n.ts.widgets" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')">
+ <i class="ti ti-apps ti-fw"></i>
+ </button>
</div>
<div :class="$style.middle">
<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
@@ -51,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</div>
<div :class="$style.bottom">
- <button v-if="showWidgetButton" v-tooltip.noDelay.right="i18n.ts.widgets" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')">
+ <button v-if="iconOnly && showWidgetButton" v-tooltip.noDelay.right="i18n.ts.widgets" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')">
<i class="ti ti-apps ti-fw"></i>
</button>
<button v-if="iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode">
@@ -436,6 +439,12 @@ function menuEdit() {
}
}
+ .widget {
+ display: inline-block;
+ width: var(--top-height);
+ margin-left: auto;
+ }
+
.bottom {
position: sticky;
bottom: 0;
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 0941c25467..484b7f277a 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -38,36 +38,39 @@ SPDX-License-Identifier: AGPL-3.0-only
@headerWheel="onWheel"
/>
</section>
- <div v-if="layout.length === 0" class="_panel" :class="$style.onboarding">
+ <div v-if="layout.length === 0" class="_panel _gaps" :class="$style.onboarding">
<div>{{ i18n.ts._deck.introduction }}</div>
<div>{{ i18n.ts._deck.introduction2 }}</div>
+ <MkInfo v-if="!store.r.tips.value.deck" closable @close="closeTip('deck')">
+ <button class="_textButton" @click="showTour">{{ i18n.ts._deck.showHowToUse }}</button>
+ </MkInfo>
</div>
</div>
<div v-if="prefer.r['deck.menuPosition'].value === 'right'" :class="$style.sideMenu">
<div :class="$style.sideMenuTop">
- <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
+ <button ref="swicthProfileButtonEl" v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
</div>
<div :class="$style.sideMenuMiddle">
- <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
+ <button ref="addColumnButtonEl" v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
</div>
<div :class="$style.sideMenuBottom">
- <button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
+ <button ref="settingsButtonEl" v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
</div>
</div>
</div>
<div v-if="prefer.r['deck.menuPosition'].value === 'bottom'" :class="$style.bottomMenu">
<div :class="$style.bottomMenuLeft">
- <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.bottomMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
- <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.bottomMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
+ <button ref="swicthProfileButtonEl" v-tooltip.noDelay.top="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.bottomMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
+ <button v-tooltip.noDelay.top="i18n.ts._deck.deleteProfile" :class="$style.bottomMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
</div>
<div :class="$style.bottomMenuMiddle">
- <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.bottomMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
+ <button ref="addColumnButtonEl" v-tooltip.noDelay.top="i18n.ts._deck.addColumn" :class="$style.bottomMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
</div>
<div :class="$style.bottomMenuRight">
- <button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
+ <button ref="settingsButtonEl" v-tooltip.noDelay.top="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
</div>
</div>
@@ -96,6 +99,7 @@ import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { deviceKind } from '@/utility/device-kind.js';
import { prefer } from '@/preferences.js';
+import { store } from '@/store.js';
import XMainColumn from '@/ui/deck/main-column.vue';
import XTlColumn from '@/ui/deck/tl-column.vue';
import XAntennaColumn from '@/ui/deck/antenna-column.vue';
@@ -107,10 +111,13 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-column.vue';
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import XChatColumn from '@/ui/deck/chat-column.vue';
+import MkInfo from '@/components/MkInfo.vue';
import { mainRouter } from '@/router.js';
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
+import { startTour } from '@/utility/tour.js';
+import { closeTip } from '@/tips.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
@@ -163,6 +170,9 @@ function showSettings() {
}
const columnsEl = useTemplateRef('columnsEl');
+const addColumnButtonEl = useTemplateRef('addColumnButtonEl');
+const settingsButtonEl = useTemplateRef('settingsButtonEl');
+const swicthProfileButtonEl = useTemplateRef('swicthProfileButtonEl');
const addColumn = async (ev) => {
const { canceled, result: column } = await os.select({
@@ -218,6 +228,30 @@ async function deleteProfile() {
os.success();
}
+function showTour() {
+ if (addColumnButtonEl.value == null ||
+ settingsButtonEl.value == null ||
+ swicthProfileButtonEl.value == null) {
+ return;
+ }
+
+ startTour([{
+ element: addColumnButtonEl.value,
+ title: i18n.ts._deck._howToUse.addColumn_title,
+ description: i18n.ts._deck._howToUse.addColumn_description,
+ }, {
+ element: settingsButtonEl.value,
+ title: i18n.ts._deck._howToUse.settings_title,
+ description: i18n.ts._deck._howToUse.settings_description,
+ }, {
+ element: swicthProfileButtonEl.value,
+ title: i18n.ts._deck._howToUse.switchProfile_title,
+ description: i18n.ts._deck._howToUse.switchProfile_description,
+ }]).then(() => {
+ closeTip('deck');
+ });
+}
+
window.document.documentElement.style.overflowY = 'hidden';
window.document.documentElement.style.scrollBehavior = 'auto';
</script>
@@ -345,7 +379,7 @@ window.document.documentElement.style.scrollBehavior = 'auto';
}
.bottomMenuButton {
- display: block;
+ display: inline-block;
height: 100%;
aspect-ratio: 1;
}
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 727fe08989..497ef72d04 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XTitlebar v-if="prefer.r.showTitlebar.value" style="flex-shrink: 0;"/>
<div :class="$style.nonTitlebarArea">
- <XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!isDesktop" @widgetButtonClick="widgetsShowing = true"/>
+ <XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!showWidgetsSide" @widgetButtonClick="widgetsShowing = true"/>
<div :class="[$style.contents, !isMobile && prefer.r.showTitlebar.value ? $style.withSidebarAndTitlebar : null]" @contextmenu.stop="onContextmenu">
<div>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XMobileFooterMenu v-if="isMobile" ref="navFooter" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
- <div v-if="isDesktop && !pageMetadata?.needWideArea" :class="$style.widgets">
+ <div v-if="showWidgetsSide && !pageMetadata?.needWideArea" :class="$style.widgets">
<XWidgets/>
</div>
</div>
@@ -64,7 +64,8 @@ const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 500;
// デスクトップでウィンドウを狭くしたときモバイルUIが表示されて欲しいことはあるので deviceKind === 'desktop' の判定は行わない
-const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
+const showWidgetsSide = window.innerWidth >= DESKTOP_THRESHOLD;
+
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
window.addEventListener('resize', () => {
isMobile.value = deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD;
@@ -102,14 +103,6 @@ if (window.innerWidth > 1024) {
}
}
-onMounted(() => {
- if (!isDesktop.value) {
- window.addEventListener('resize', () => {
- if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
- }, { passive: true });
- }
-});
-
const onContextmenu = (ev) => {
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
diff --git a/packages/frontend/src/utility/storage.ts b/packages/frontend/src/utility/storage.ts
new file mode 100644
index 0000000000..9df3a251e6
--- /dev/null
+++ b/packages/frontend/src/utility/storage.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
+import * as os from '@/os.js';
+import { store } from '@/store.js';
+import { i18n } from '@/i18n.js';
+
+export const storagePersisted = ref(await navigator.storage.persisted());
+
+export async function enableStoragePersistence() {
+ try {
+ const persisted = await navigator.storage.persist();
+ if (persisted) {
+ storagePersisted.value = true;
+ } else {
+ os.alert({
+ type: 'error',
+ text: i18n.ts.somethingHappened,
+ });
+ }
+ } catch (err) {
+ os.alert({
+ type: 'error',
+ text: i18n.ts.somethingHappened,
+ });
+ }
+}
+
+export function skipStoragePersistence() {
+ store.set('showStoragePersistenceSuggestion', false);
+}