enhance(frontend): テーマ切り替えのアニメーションをView Transitionに変更 (#15974)

* enhance(frontend): テーマ切り替えのアニメーションをView Transitionに変更

* fix lint

* fix: 切り替え時間を0.5sに
This commit is contained in:
かっこかり 2025-05-21 14:19:34 +09:00 committed by GitHub
parent bd7633c70e
commit 2619f69238
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 19 deletions

View File

@ -31,9 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, ref, useTemplateRef, watch } from 'vue';
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
import { miLocalStorage } from '@/local-storage.js';
import { prefer } from '@/preferences.js';
import { globalEvents } from '@/events.js';
import { getBgColor } from '@/utility/get-bg-color.js';
const miLocalStoragePrefix = 'ui:folder:' as const;
@ -83,8 +84,19 @@ function afterLeave(el: Element) {
el.style.height = '';
}
function updateBgColor() {
if (rootEl.value) {
parentBg.value = getBgColor(rootEl.value.parentElement);
}
}
onMounted(() => {
parentBg.value = getBgColor(rootEl.value?.parentElement);
updateBgColor();
globalEvents.on('themeChanging', updateBgColor);
});
onBeforeUnmount(() => {
globalEvents.off('themeChanging', updateBgColor);
});
</script>

View File

@ -90,12 +90,49 @@ html {
}
}
html._themeChanging_ {
html._themeChangingFallback_ {
&, * {
transition: background 1s ease, border 1s ease !important;
transition: background 0.5s ease, border 0.5s ease !important;
}
}
html._themeChanging_ {
view-transition-name: theme-changing;
}
html::view-transition-new(theme-changing) {
z-index: 4000001;
animation: themeChangingNew 0.5s ease;
animation-fill-mode: forwards;
}
html::view-transition-old(theme-changing) {
z-index: 4000000;
animation: themeChangingOld 0.5s ease;
animation-fill-mode: forwards;
}
@keyframes themeChangingNew {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes themeChangingOld {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
html,
body,
#misskey_app {

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref } from 'vue';
import { ref, nextTick } from 'vue';
import tinycolor from 'tinycolor2';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
@ -88,20 +88,7 @@ export async function removeTheme(theme: Theme): Promise<void> {
prefer.commit('themes', themes);
}
let timeout: number | null = null;
export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout);
window.document.documentElement.classList.add('_themeChanging_');
timeout = window.setTimeout(() => {
window.document.documentElement.classList.remove('_themeChanging_');
// 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanged');
}, 1000);
function applyThemeInternal(theme: Theme, persist: boolean) {
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
window.document.documentElement.dataset.colorScheme = colorScheme;
@ -139,6 +126,37 @@ export function applyTheme(theme: Theme, persist = true) {
globalEvents.emit('themeChanging');
}
let timeout: number | null = null;
export function applyTheme(theme: Theme, persist = true) {
if (timeout) {
window.clearTimeout(timeout);
timeout = null;
}
if (window.document.startViewTransition != null && prefer.s.animation) {
window.document.documentElement.classList.add('_themeChanging_');
window.document.startViewTransition(async () => {
applyThemeInternal(theme, persist);
await nextTick();
}).finished.then(() => {
window.document.documentElement.classList.remove('_themeChanging_');
// 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanged');
});
} else {
// TODO: ViewTransition API が主要ブラウザで対応したら消す
window.document.documentElement.classList.add('_themeChangingFallback_');
timeout = window.setTimeout(() => {
window.document.documentElement.classList.remove('_themeChangingFallback_');
// 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanged');
}, 500);
applyThemeInternal(theme, persist);
}
}
export function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)