diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2025-12-22 05:30:45 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-22 05:30:45 +0000 |
| commit | 0d46089f9a18abbb001fee2860dfaabf881831b3 (patch) | |
| tree | 8315f33781b790084279680d05ea521f47fe1219 /packages/frontend/src | |
| parent | Merge pull request #16972 from misskey-dev/develop (diff) | |
| parent | Release: 2025.12.2 (diff) | |
| download | misskey-0d46089f9a18abbb001fee2860dfaabf881831b3.tar.gz misskey-0d46089f9a18abbb001fee2860dfaabf881831b3.tar.bz2 misskey-0d46089f9a18abbb001fee2860dfaabf881831b3.zip | |
Merge pull request #16998 from misskey-dev/develop
Release: 2025.12.2
Diffstat (limited to 'packages/frontend/src')
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); +} |