Merge branch 'develop' into mkjs-n

This commit is contained in:
tamaina
2023-06-04 13:31:14 +00:00
127 changed files with 7780 additions and 3638 deletions

View File

@@ -1,6 +1,5 @@
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue';
import { compareVersions } from 'compare-versions';
import JSON5 from 'json5';
import widgets from '@/widgets';
import directives from '@/directives';
import components from '@/components';
@@ -180,8 +179,8 @@ export async function common(createVue: () => App<Element>) {
fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
});

View File

@@ -3,7 +3,14 @@
<div v-if="achievements" :class="$style.root">
<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
<div :class="$style.icon">
<div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
<div
:class="[$style.iconFrame, {
[$style.iconFrame_bronze]: ACHIEVEMENT_BADGES[achievement.name].frame === 'bronze',
[$style.iconFrame_silver]: ACHIEVEMENT_BADGES[achievement.name].frame === 'silver',
[$style.iconFrame_gold]: ACHIEVEMENT_BADGES[achievement.name].frame === 'gold',
[$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum',
}]"
>
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div>

View File

@@ -10,7 +10,7 @@
</li>
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
</ol>
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]">
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
<span class="name">{{ hashtag }}</span>
</li>
@@ -42,7 +42,7 @@ import { acct } from '@/filters/user';
import * as os from '@/os';
import { MFM_TAGS } from '@/scripts/mfm-tags';
import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist';
import { emojilist, getEmojiName } from '@/scripts/emojilist';
import { i18n } from '@/i18n';
import { miLocalStorage } from '@/local-storage';
import { customEmojis } from '@/custom-emojis';
@@ -71,14 +71,14 @@ const emojiDb = computed(() => {
url: char2path(x.char),
}));
for (const x of lib) {
if (x.keywords) {
for (const k of x.keywords) {
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const [emoji, keywords] of Object.entries(index)) {
for (const k of keywords) {
unicodeEmojiDB.push({
emoji: x.char,
emoji: emoji,
name: k,
aliasOf: x.name,
url: char2path(x.char),
aliasOf: getEmojiName(emoji)!,
url: char2path(emoji),
});
}
}

View File

@@ -7,7 +7,7 @@
@click="emit('click', $event)"
@mousedown="onMousedown"
>
<div ref="ripples" :class="$style.ripples"></div>
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content">
<slot></slot>
</div>
@@ -18,7 +18,7 @@
:to="to"
@mousedown="onMousedown"
>
<div ref="ripples" :class="$style.ripples"></div>
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content">
<slot></slot>
</div>
@@ -26,9 +26,7 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, useCssModule } from 'vue';
const $style = useCssModule();
import { nextTick, onMounted } from 'vue';
const props = defineProps<{
type?: 'button' | 'submit' | 'reset';
@@ -81,7 +79,7 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect();
const ripple = document.createElement('div');
ripple.classList.add($style.ripple);
ripple.classList.add(ripples!.dataset.childrenClass!);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';

View File

@@ -1,20 +1,20 @@
<template>
<button
class="hdcaacmi _button"
:class="{ wait, active: isFollowing, full }"
class="_button"
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full }]"
:disabled="wait"
@click="onClick"
>
<template v-if="!wait">
<template v-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
</template>
<template v-else>
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
</template>
</template>
<template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
</template>
</button>
</template>
@@ -57,8 +57,8 @@ async function onClick() {
}
</script>
<style lang="scss" scoped>
.hdcaacmi {
<style lang="scss" module>
.root {
position: relative;
display: inline-block;
font-weight: bold;
@@ -103,7 +103,7 @@ async function onClick() {
}
&.active {
color: #fff;
color: var(--fgOnAccent);
background: var(--accent);
&:hover {
@@ -121,9 +121,9 @@ async function onClick() {
cursor: wait !important;
opacity: 0.7;
}
}
> span {
margin-right: 6px;
}
.text {
margin-right: 6px;
}
</style>

View File

@@ -3,7 +3,7 @@
<div v-if="game.ready" :class="$style.game">
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
<button v-click-anime class="_button" :class="$style.button" @click="onClick">
<button v-click-anime class="_button" @click="onClick">
<img src="/client-assets/cookie.png" :class="$style.img">
</button>
</div>
@@ -84,10 +84,6 @@ onUnmounted(() => {
margin-bottom: 6px;
}
.button {
}
.img {
max-width: 90px;
}

View File

@@ -1,5 +1,5 @@
<template>
<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]">
<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.scrollable]: scrollable }]">
<header v-if="showHeader" ref="headerEl" :class="$style.header">
<div :class="$style.title">
<span :class="$style.titleIcon"><slot name="icon"></slot></span>
@@ -34,7 +34,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue';
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -83,13 +83,19 @@ function afterLeave(el) {
const calcOmit = () => {
if (omitted.value || ignoreOmit.value || props.maxHeight == null) return;
if (!contentEl.value) return;
const height = contentEl.value.offsetHeight;
omitted.value = height > props.maxHeight;
};
const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit();
});
onMounted(() => {
watch(showBody, v => {
const headerHeight = props.showHeader ? headerEl.value.offsetHeight : 0;
if (!rootEl.value) return;
const headerHeight = props.showHeader ? headerEl.value?.offsetHeight ?? 0 : 0;
rootEl.value.style.minHeight = `${headerHeight}px`;
if (v) {
rootEl.value.style.flexBasis = 'auto';
@@ -100,13 +106,15 @@ onMounted(() => {
immediate: true,
});
rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
if (rootEl.value) rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
calcOmit();
new ResizeObserver((entries, observer) => {
calcOmit();
}).observe(contentEl.value);
if (contentEl.value) omitObserver.observe(contentEl.value);
});
onUnmounted(() => {
omitObserver.disconnect();
});
</script>

View File

@@ -36,7 +36,7 @@ export default defineComponent({
},
setup(props, { slots, expose }) {
const $style = useCssModule();
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;

View File

@@ -4,7 +4,15 @@
<div v-if="icon" :class="$style.icon">
<i :class="icon"></i>
</div>
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
<div
v-else-if="!input && !select"
:class="[$style.icon, {
[$style.type_success]: type === 'success',
[$style.type_error]: type === 'error',
[$style.type_warning]: type === 'warning',
[$style.type_info]: type === 'info',
}]"
>
<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>

View File

@@ -1,7 +1,8 @@
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<div ref="emojisEl" class="emojis">
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
<div ref="emojisEl" class="emojis" tabindex="-1">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
@@ -101,7 +102,7 @@ import { isTouchUsing } from '@/scripts/touch';
import { deviceKind } from '@/scripts/device-kind';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { customEmojiCategories, customEmojis } from '@/custom-emojis';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis';
import { $i } from '@/account';
const props = withDefaults(defineProps<{
@@ -223,7 +224,6 @@ watch(q, () => {
if (newQ.includes(' ')) { // AND検索
const keywords = newQ.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
@@ -232,11 +232,12 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
// 名前またはエイリアスにキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
} else {
@@ -248,13 +249,14 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
@@ -264,10 +266,12 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
}
@@ -352,7 +356,7 @@ function done(query?: string): boolean | void {
if (query == null || typeof query !== 'string') return;
const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2);
const exactMatchCustom = customEmojisMap.get(q2);
if (exactMatchCustom) {
chosen(exactMatchCustom);
return true;

View File

@@ -5,7 +5,7 @@
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
<div :class="$style.headerText">
<div :class="$style.headerTextMain">
<div>
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
</div>
<div :class="$style.headerTextSub">
@@ -185,10 +185,6 @@ onMounted(() => {
padding-right: 12px;
}
.headerTextMain {
}
.headerTextSub {
color: var(--fgTransparentWeak);
font-size: .85em;

View File

@@ -1,30 +1,30 @@
<template>
<button
class="kpoogebi _button"
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
class="_button"
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
:disabled="wait"
@click="onClick"
>
<template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
<!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template>
<template v-else-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
</template>
</template>
<template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template>
</button>
</template>
@@ -126,13 +126,12 @@ onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.kpoogebi {
<style lang="scss" module>
.root {
position: relative;
display: inline-block;
font-weight: bold;
color: var(--accent);
background: transparent;
color: var(--fgOnWhite);
border: solid 1px var(--accent);
padding: 0;
height: 31px;
@@ -196,9 +195,9 @@ onBeforeUnmount(() => {
cursor: wait !important;
opacity: 0.7;
}
}
> span {
margin-right: 6px;
}
.text {
margin-right: 6px;
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<div ref="root" :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
<TransitionGroup
:duration="defaultStore.state.animation && props.transition?.duration || undefined"
:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined"
:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
@@ -23,6 +23,11 @@ import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
// テスト環境で Web Worker インスタンスは作成できない
if (import.meta.env.MODE === 'test') {
resolve(null);
return;
}
const testWorker = new TestWebGL2();
testWorker.addEventListener('message', event => {
if (event.data.result) {
@@ -42,11 +47,10 @@ const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
</script>
<script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, shallowRef, useCssModule, watch } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { v4 as uuid } from 'uuid';
import { render } from 'buraha';
import { defaultStore } from '@/store';
const $style = useCssModule();
const props = withDefaults(defineProps<{
transition?: {

View File

@@ -1,27 +1,27 @@
<template>
<div class="mk-media-banner">
<div v-if="media.isSensitive && hide" class="sensitive" @click="hide = false">
<span class="icon"><i class="ti ti-alert-triangle"></i></span>
<div :class="$style.root">
<div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
<VuePlyr :options="{ volume: 0.5 }">
<audio controls preload="metadata">
<source
:src="media.url"
:type="media.type"
/>
</audio>
</VuePlyr>
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :class="$style.audio">
<audio
ref="audioEl"
:src="media.url"
:title="media.name"
controls
preload="metadata"
@volumechange="volumechange"
/>
</div>
<a
v-else class="download"
v-else :class="$style.download"
:href="media.url"
:title="media.name"
:download="media.name"
>
<span class="icon"><i class="ti ti-download"></i></span>
<span style="font-size: 1.6em;"><i class="ti ti-download"></i></span>
<b>{{ media.name }}</b>
</a>
</div>
@@ -30,9 +30,7 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import { soundConfigStore } from '@/scripts/sound';
import 'vue-plyr/dist/vue-plyr.css';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
@@ -52,55 +50,34 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
.mk-media-banner {
<style lang="scss" module>
.root {
width: 100%;
border-radius: 4px;
margin-top: 4px;
// overflow: clip;
overflow: clip;
}
--plyr-color-main: var(--accent);
--plyr-audio-controls-background: var(--bg);
--plyr-audio-controls-color: var(--accentLighten);
.download,
.sensitive {
display: flex;
align-items: center;
font-size: 12px;
padding: 8px 12px;
white-space: nowrap;
}
> .download,
> .sensitive {
display: flex;
align-items: center;
font-size: 12px;
padding: 8px 12px;
white-space: nowrap;
.download {
background: var(--noteAttachedFile);
}
> * {
display: block;
}
.sensitive {
background: #111;
color: #fff;
}
> b {
overflow: hidden;
text-overflow: ellipsis;
}
> *:not(:last-child) {
margin-right: .2em;
}
> .icon {
font-size: 1.6em;
}
}
> .download {
background: var(--noteAttachedFile);
}
> .sensitive {
background: #111;
color: #fff;
}
> .audio {
border-radius: 8px;
// overflow: clip;
}
.audio {
border-radius: 8px;
overflow: clip;
}
</style>

View File

@@ -32,8 +32,7 @@
<div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
</div>
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click.stop.prevent="hide = true"><i class="ti ti-eye-off"></i></button>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
</template>
</div>
</template>
@@ -79,9 +78,16 @@ watch(() => props.image, () => {
});
function showMenu(ev: MouseEvent) {
os.popupMenu([...(iAmModerator ? [{
text: i18n.ts.markAsSensitive,
os.popupMenu([{
text: i18n.ts.hide,
icon: 'ti ti-eye-off',
action: () => {
hide = true;
},
}, ...(iAmModerator ? [{
text: i18n.ts.markAsSensitive,
icon: 'ti ti-eye-exclamation',
danger: true,
action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
},
@@ -122,34 +128,20 @@ function showMenu(ev: MouseEvent) {
background-size: 16px 16px;
}
.hide {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
top: 12px;
right: 12px;
}
.menu {
display: block;
position: absolute;
border-radius: 6px;
border-radius: 999px;
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: #fff;
font-size: 0.8em;
padding: 6px 8px;
width: 32px;
height: 32px;
text-align: center;
bottom: 12px;
right: 12px;
bottom: 10px;
right: 10px;
}
.imageContainer {
@@ -166,12 +158,10 @@ function showMenu(ev: MouseEvent) {
.indicators {
display: inline-flex;
position: absolute;
top: 12px;
left: 12px;
text-align: center;
top: 10px;
left: 10px;
pointer-events: none;
opacity: .5;
font-size: 14px;
gap: 6px;
}
@@ -182,7 +172,7 @@ function showMenu(ev: MouseEvent) {
color: var(--accentLighten);
display: inline-block;
font-weight: bold;
font-size: 12px;
padding: 2px 6px;
font-size: 0.8em;
padding: 2px 5px;
}
</style>

View File

@@ -6,8 +6,11 @@
ref="gallery"
:class="[
$style.medias,
count <= 4 ? $style['n' + count] : $style.nMany,
$style[`n1${defaultStore.reactiveState.mediaListWithOneImageAppearance.value}`]
count === 1 ? [$style.n1, {
[$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9',
[$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1',
[$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3',
}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
]"
>
<template v-for="media in mediaList.filter(media => previewable(media))">
@@ -20,7 +23,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, useCssModule, watch, shallowRef } from 'vue';
import { onMounted, watch, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@@ -37,8 +40,6 @@ const props = defineProps<{
raw?: boolean;
}>();
const $style = useCssModule();
const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
@@ -96,7 +97,7 @@ onMounted(() => {
return item;
}),
gallery: gallery.value,
mainClass: $style.pswp,
mainClass: 'pswp',
children: '.image',
thumbSelector: '.image',
loop: false,
@@ -268,7 +269,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
border-radius: 8px;
}
.pswp {
:global(.pswp) {
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
--pswp-bg: var(--modalBg) !important;
}

View File

@@ -1,36 +1,36 @@
<template>
<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false">
<div v-if="hide" :class="$style.hidden" @click="hide = false">
<!-- 注意dataSaverMode が有効になっている際にはhide false になるまでサムネイルや動画を読み込まないようにすること -->
<div>
<b v-if="video.isSensitive"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
<b v-else><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
<div :class="$style.sensitive">
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
<VuePlyr :options="{ volume: 0.5 }">
<video
controls
:data-poster="video.thumbnailUrl"
<div v-else :class="$style.visible">
<video
:class="$style.video"
:poster="video.thumbnailUrl"
:title="video.comment"
:alt="video.comment"
preload="none"
controls
@contextmenu.stop
>
<source
:src="video.url"
:type="video.type"
>
<source
size="720"
:src="video.url"
:type="video.type"
/>
</video>
</VuePlyr>
<i class="ti ti-eye-off" @click="hide = true"></i>
</video>
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import bytes from '@/filters/bytes';
import { defaultStore } from '@/store';
import 'vue-plyr/dist/vue-plyr.css';
import { i18n } from '@/i18n';
const props = defineProps<{
@@ -40,56 +40,49 @@ const props = defineProps<{
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
</script>
<style lang="scss" scoped>
.kkjnbbplepmiyuadieoenjgutgcmtsvu {
<style lang="scss" module>
.visible {
position: relative;
--plyr-color-main: var(--accent);
> i {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--fg);
color: var(--accentLighten);
font-size: 14px;
opacity: .5;
padding: 3px 6px;
text-align: center;
cursor: pointer;
top: 12px;
right: 12px;
}
> video {
display: flex;
justify-content: center;
align-items: center;
font-size: 3.5em;
overflow: hidden;
background-position: center;
background-size: cover;
width: 100%;
height: 100%;
}
}
.icozogqfvdetwohsdglrbswgrejoxbdj {
.hide {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--fg);
color: var(--accentLighten);
font-size: 14px;
opacity: .5;
padding: 3px 6px;
text-align: center;
cursor: pointer;
top: 12px;
right: 12px;
}
.video {
display: flex;
justify-content: center;
align-items: center;
font-size: 3.5em;
overflow: hidden;
background-position: center;
background-size: cover;
width: 100%;
height: 100%;
}
.hidden {
display: flex;
justify-content: center;
align-items: center;
background: #111;
color: #fff;
}
> div {
display: table-cell;
text-align: center;
font-size: 12px;
> b {
display: block;
}
}
.sensitive {
display: table-cell;
text-align: center;
font-size: 12px;
}
</style>

View File

@@ -2,7 +2,7 @@
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<span>
<span :class="$style.username">@{{ username }}</span>
<span>@{{ username }}</span>
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span>
</span>
</MkA>

View File

@@ -49,7 +49,7 @@
<span>{{ i18n.ts.none }}</span>
</span>
</div>
<div v-if="childMenu" :class="$style.child">
<div v-if="childMenu">
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
</div>
</div>

View File

@@ -1,10 +1,30 @@
<template>
<Transition
:name="transitionName"
:enterActiveClass="$style['transition_' + transitionName + '_enterActive']"
:leaveActiveClass="$style['transition_' + transitionName + '_leaveActive']"
:enterFromClass="$style['transition_' + transitionName + '_enterFrom']"
:leaveToClass="$style['transition_' + transitionName + '_leaveTo']"
:enterActiveClass="normalizeClass({
[$style.transition_modalDrawer_enterActive]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_enterActive]: transitionName === 'modal-popup',
[$style.transition_modal_enterActive]: transitionName === 'modal',
[$style.transition_send_enterActive]: transitionName === 'send',
})"
:leaveActiveClass="normalizeClass({
[$style.transition_modalDrawer_leaveActive]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_leaveActive]: transitionName === 'modal-popup',
[$style.transition_modal_leaveActive]: transitionName === 'modal',
[$style.transition_send_leaveActive]: transitionName === 'send',
})"
:enterFromClass="normalizeClass({
[$style.transition_modalDrawer_enterFrom]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_enterFrom]: transitionName === 'modal-popup',
[$style.transition_modal_enterFrom]: transitionName === 'modal',
[$style.transition_send_enterFrom]: transitionName === 'send',
})"
:leaveToClass="normalizeClass({
[$style.transition_modalDrawer_leaveTo]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_leaveTo]: transitionName === 'modal-popup',
[$style.transition_modal_leaveTo]: transitionName === 'modal',
[$style.transition_send_leaveTo]: transitionName === 'send',
})"
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
>
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
@@ -17,7 +37,7 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, watch, provide } from 'vue';
import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { defaultStore } from '@/store';
@@ -38,7 +58,7 @@ type ModalTypes = 'popup' | 'dialog' | 'drawer';
const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
anchor?: { x: string; y: string; };
src?: HTMLElement;
src?: HTMLElement | null;
preferType?: ModalTypes | 'auto';
zPriority?: 'low' | 'middle' | 'high';
noOverlap?: boolean;
@@ -264,6 +284,10 @@ const onOpened = () => {
}, { passive: true });
};
const alignObserver = new ResizeObserver((entries, observer) => {
align();
});
onMounted(() => {
watch(() => props.src, async () => {
if (props.src) {
@@ -278,12 +302,14 @@ onMounted(() => {
}, { immediate: true });
nextTick(() => {
new ResizeObserver((entries, observer) => {
align();
}).observe(content!);
alignObserver.observe(content!);
});
});
onUnmounted(() => {
alignObserver.disconnect();
});
defineExpose({
close,
});
@@ -339,8 +365,8 @@ defineExpose({
}
}
.transition_modal-popup_enterActive,
.transition_modal-popup_leaveActive {
.transition_modalPopup_enterActive,
.transition_modalPopup_leaveActive {
> .bg {
transition: opacity 0.1s !important;
}
@@ -350,8 +376,8 @@ defineExpose({
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
}
}
.transition_modal-popup_enterFrom,
.transition_modal-popup_leaveTo {
.transition_modalPopup_enterFrom,
.transition_modalPopup_leaveTo {
> .bg {
opacity: 0;
}
@@ -364,7 +390,7 @@ defineExpose({
}
}
.transition_modal-drawer_enterActive {
.transition_modalDrawer_enterActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -373,7 +399,7 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.transition_modal-drawer_leaveActive {
.transition_modalDrawer_leaveActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -382,8 +408,8 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.transition_modal-drawer_enterFrom,
.transition_modal-drawer_leaveTo {
.transition_modalDrawer_enterFrom,
.transition_modalDrawer_leaveTo {
> .bg {
opacity: 0;
}

View File

@@ -44,8 +44,8 @@
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/>
<MkNoteHeader :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
@@ -58,13 +58,13 @@
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else :class="$style.translated">
<div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div>
</div>
</div>
<div v-if="appearNote.files.length > 0" :class="$style.files">
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
@@ -205,8 +205,11 @@ const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const isLong = (appearNote.cw == null && appearNote.text != null && (
(appearNote.text.includes('$[x2')) ||
(appearNote.text.includes('$[x3')) ||
(appearNote.text.includes('$[x4')) ||
(appearNote.text.includes('$[scale')) ||
(appearNote.text.includes('$[position')) ||
(appearNote.text.split('\n').length > 9) ||
(appearNote.text.length > 500) ||
(appearNote.files.length >= 5) ||
@@ -274,7 +277,7 @@ function renote(viaKeyboard = false) {
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
@@ -305,7 +308,7 @@ function renote(viaKeyboard = false) {
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
os.api('notes/create', {
renoteId: appearNote.id,
}).then(() => {
@@ -379,6 +382,8 @@ function undoReact(note): void {
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
if (el.tagName === 'AUDIO') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}

View File

@@ -4,7 +4,7 @@
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
:class="[$style.root, { [$style.renote]: isRenote }]"
:class="$style.root"
>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
@@ -52,7 +52,7 @@
</div>
</div>
<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
</div>
</header>
<div :class="$style.noteContent">
@@ -72,7 +72,7 @@
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div>
</div>
<div v-if="appearNote.files.length > 0" :class="$style.files">
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>

View File

@@ -6,7 +6,7 @@
<MkUserName :user="$i" :nowrap="true"/>
</div>
<div>
<div :class="$style.content">
<div>
<Mfm :text="text.trim()" :author="$i" :i="$i"/>
</div>
</div>

View File

@@ -5,7 +5,19 @@
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
<div :class="[$style.subIcon, $style['t_' + notification.type]]">
<div
:class="[$style.subIcon, {
[$style.t_follow]: notification.type === 'follow',
[$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted',
[$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest',
[$style.t_renote]: notification.type === 'renote',
[$style.t_reply]: notification.type === 'reply',
[$style.t_mention]: notification.type === 'mention',
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
}]"
>
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
@@ -34,7 +46,7 @@
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header>
<div :class="$style.content">
<div>
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
@@ -243,9 +255,6 @@ useTooltip(reactionRef, (showing) => {
font-size: 0.9em;
}
.content {
}
.text {
display: flex;
width: 100%;

View File

@@ -8,7 +8,7 @@
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { onMounted, onUnmounted } from 'vue';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
@@ -21,16 +21,22 @@ let content = $shallowRef<HTMLElement>();
let omitted = $ref(false);
let ignoreOmit = $ref(false);
onMounted(() => {
const calcOmit = () => {
if (omitted || ignoreOmit) return;
omitted = content.offsetHeight > props.maxHeight;
};
const calcOmit = () => {
if (omitted || ignoreOmit) return;
omitted = content.offsetHeight > props.maxHeight;
};
const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit();
new ResizeObserver((entries, observer) => {
calcOmit();
}).observe(content);
});
onMounted(() => {
calcOmit();
omitObserver.observe(content);
});
onUnmounted(() => {
omitObserver.disconnect();
});
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div :class="{ [$style.done]: closed || isVoted }">
<ul :class="$style.choices">
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="[$style.choice, { [$style.voted]: choice.voted }]" @click="vote(i)">
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>

View File

@@ -22,21 +22,21 @@
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span>
</button>
<button v-else :class="['_button', $style.headerRightItem, $style.visibility]" disabled>
<button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled>
<span><i class="ti ti-device-tv"></i></span>
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
</button>
</template>
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" :class="['_button', $style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-else><i class="ti ti-rocket-off"></i></span>
</button>
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" :class="['_button', $style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ti ti-icons"></i></span>
</button>
<button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div :class="$style.submitInner">
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
@@ -66,7 +66,7 @@
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
<div v-if="showingOptions" style="padding: 8px 16px;">

View File

@@ -93,7 +93,7 @@ function showFileMenu(file, ev: MouseEvent) {
action: () => { rename(file); },
}, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: file.isSensitive ? 'ti ti-eye-off' : 'ti ti-eye',
icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye',
action: () => { toggleSensitive(file); },
}, {
text: i18n.ts.describeFile,

View File

@@ -1,6 +1,6 @@
<template>
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div :class="$style.body">
<div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
@@ -76,10 +76,6 @@ const collapsed = $ref(
}
}
.body {
}
.reply {
margin-right: 6px;
color: var(--accent);

View File

@@ -6,7 +6,7 @@
ref="inputEl"
v-model="v"
v-adaptive-border
:class="[$style.textarea, { [$style.code]: code, _monospace: code }]"
:class="[$style.textarea, { _monospace: code }]"
:disabled="disabled"
:required="required"
:readonly="readonly"

View File

@@ -22,7 +22,7 @@
</div>
</template>
<template v-else-if="tweetId && tweetExpanded">
<div ref="twitter" :class="$style.twitter">
<div ref="twitter">
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div>
<div :class="$style.action">
@@ -31,7 +31,7 @@
</MkButton>
</div>
</template>
<div v-else :class="$style.urlPreview">
<div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
</div>
@@ -210,13 +210,6 @@ onUnmounted(() => {
width: 100%;
}
.twitter {
}
.urlPreview {
}
.link {
position: relative;
display: block;

View File

@@ -8,7 +8,7 @@
</div>
<span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
<div :class="$style.description">
<div v-if="user.description" class="mfm">
<div v-if="user.description" :class="$style.mfm">
<Mfm :text="user.description" :author="user" :i="$i"/>
</div>
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>

View File

@@ -1,5 +1,13 @@
<template>
<div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div>
<div
v-tooltip="text"
:class="[$style.root, {
[$style.status_online]: user.onlineStatus === 'online',
[$style.status_active]: user.onlineStatus === 'active',
[$style.status_offline]: user.onlineStatus === 'offline',
[$style.status_unknown]: user.onlineStatus === 'unknown',
}]"
></div>
</template>
<script lang="ts" setup>

View File

@@ -9,7 +9,7 @@
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.selectUser }}</template>
<div :class="$style.root">
<div>
<div :class="$style.form">
<FormSplit :minWidth="170">
<MkInput v-model="username" :autofocus="true" @update:modelValue="search">
@@ -126,8 +126,6 @@ onMounted(() => {
</script>
<style lang="scss" module>
.root {
}
.form {
padding: 0 var(--root-margin);

View File

@@ -3,9 +3,9 @@
<div :class="$style.root">
<div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar :class="$style.avatar" :user="u"/>
<MkUserName :class="$style.name" :user="u" :nowrap="true"/>
<MkUserName :user="u" :nowrap="true"/>
</div>
<div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div>
<div v-if="users.length < count">+{{ count - users.length }}</div>
</div>
</MkTooltip>
</template>
@@ -43,14 +43,6 @@ const emit = defineEmits<{
}
}
.name {
}
.omitted {
}
.avatar {
width: 24px;
height: 24px;

View File

@@ -39,7 +39,7 @@
<MkTimeline src="local"/>
</div>
</div>
<div :class="[$style.activeUsersChart, $style.panel]">
<div :class="$style.panel">
<XActiveUsersChart/>
</div>
</div>
@@ -220,8 +220,4 @@ function exploreOtherServers() {
height: 350px;
overflow: auto;
}
.activeUsersChart {
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div :class="$style.root">
<template v-if="edit">
<header :class="$style['edit-header']">
<header :class="$style.editHeader">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
@@ -15,15 +15,15 @@
handle=".handle"
:animation="150"
:group="{ name: 'SortableMkWidgets' }"
:class="$style['edit-editing']"
:class="$style.editEditing"
@update:modelValue="v => emit('updateWidgets', v)"
>
<template #item="{element}">
<div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container>
<button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
<button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div class="handle">
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div>
</div>
</template>
@@ -130,7 +130,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
}
.edit {
&-header {
&Header {
margin: 16px 0;
> * {
@@ -139,17 +139,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
}
}
&-editing {
&Editing {
min-height: 100px;
}
}
.customize-container {
.customizeContainer {
position: relative;
cursor: move;
&-config,
&-remove {
&Config,
&Remove {
position: absolute;
z-index: 10000;
top: 8px;
@@ -160,17 +160,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
border-radius: 4px;
}
&-config {
&Config {
right: 8px + 8px + 32px;
}
&-remove {
&Remove {
right: 8px;
}
&-handle {
&Handle {
&-widget {
&Widget {
pointer-events: none;
}
}

View File

@@ -5,7 +5,7 @@
<span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-external-link" :class="$style.suffixIcon"></i>
<i class="ti ti-external-link"></i>
</span>
</a>
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
@@ -13,7 +13,7 @@
<span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-chevron-right" :class="$style.suffixIcon"></i>
<i class="ti ti-chevron-right"></i>
</span>
</MkA>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div :class="$style.content">
<div>
<slot></slot>
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>

View File

@@ -1,6 +1,14 @@
<template>
<div v-if="chosen && !shouldHide" :class="$style.root">
<div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
<div
v-if="!showMenu"
:class="[$style.main, {
[$style.form_square]: chosen.place === 'square',
[$style.form_horizontal]: chosen.place === 'horizontal',
[$style.form_horizontalBig]: chosen.place === 'horizontal-big',
[$style.form_vertical]: chosen.place === 'vertical',
}]"
>
<a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
@@ -122,7 +130,7 @@ function reduceFrequency(): void {
}
}
&.form_horizontal-big {
&.form_horizontalBig {
padding: 8px;
> .link,

View File

@@ -7,7 +7,7 @@
import { computed } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy';
import { defaultStore } from '@/store';
import { customEmojis } from '@/custom-emojis';
import { customEmojisMap } from '@/custom-emojis';
const props = defineProps<{
name: string;
@@ -26,7 +26,7 @@ const rawUrl = computed(() => {
return props.url;
}
if (isLocal.value) {
return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
return customEmojisMap.get(customEmojiName.value)?.url ?? null;
}
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
});

View File

@@ -14,6 +14,7 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
import { $$ } from 'vue/macros';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
const rootEl = $shallowRef<HTMLElement>();
@@ -83,8 +84,8 @@ onMounted(() => {
onUnmounted(() => {
observer.disconnect();
});
defineExpose({
rootEl: $$(rootEl),
});
</script>
<style lang="scss" module>
</style>

View File

@@ -6,7 +6,7 @@
<template v-if="!self">
<span :class="$style.schema">{{ schema }}//</span>
<span :class="$style.hostname">{{ hostname }}</span>
<span v-if="port != ''" :class="$style.port">:{{ port }}</span>
<span v-if="port != ''">:{{ port }}</span>
</template>
<template v-if="pathname === '/' && self">
<span :class="$style.self">{{ hostname }}</span>

View File

@@ -1,5 +1,5 @@
<template>
<component :is="'x-' + block.type" :key="block.id" :page="page" :block="block" :h="h"/>
<component :is="getComponent(block.type)" :key="block.id" :page="page" :block="block" :h="h"/>
</template>
<script lang="ts" setup>
@@ -11,6 +11,16 @@ import XImage from './page.image.vue';
import XNote from './page.note.vue';
import { Block } from './block.type';
function getComponent(type: string) {
switch (type) {
case 'text': return XText;
case 'section': return XSection;
case 'image': return XImage;
case 'note': return XNote;
default: return null;
}
}
defineProps<{
block: Block,
h: number,

View File

@@ -1,6 +1,15 @@
<template>
<section>
<component :is="'h' + h" :class="h < 5 ? $style['h' + h] : null">{{ block.title }}</component>
<component
:is="'h' + h"
:class="{
'h2': h === 2,
'h3': h === 3,
'h4': h === 4,
}"
>
{{ block.title }}
</component>
<div class="_gaps">
<XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/>

View File

@@ -1,6 +1,6 @@
<template>
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }">
<XBlock v-for="child in page.content" :key="child.id" :block="child" :h="2"/>
<XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/>
</div>
</template>

View File

@@ -1,4 +1,4 @@
import { shallowRef, computed, markRaw } from 'vue';
import { shallowRef, computed, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { api, apiGet } from './os';
import { useStream } from '@/stream';
@@ -16,6 +16,14 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
return markRaw([...Array.from(categories), null]);
});
export const customEmojisMap = new Map<string, Misskey.entities.CustomEmoji>();
watch(customEmojis, emojis => {
customEmojisMap.clear();
for (const emoji of emojis) {
customEmojisMap.set(emoji.name, emoji);
}
}, { immediate: true });
// TODO: ここら辺副作用なのでいい感じにする
const stream = useStream();

File diff suppressed because it is too large Load Diff

View File

@@ -149,6 +149,12 @@ const patronsWithIcon = [{
}, {
name: 'かみらえっと',
icon: 'https://misskey-hub.net/patrons/be1326bda7d940a482f3758ffd9ffaf6.jpg',
}, {
name: 'へてて',
icon: 'https://misskey-hub.net/patrons/0431eacd7c6843d09de8ea9984307e86.jpg',
}, {
name: 'spinlock',
icon: 'https://misskey-hub.net/patrons/6a1cebc819d540a78bf20e9e3115baa8.jpg',
}];
const patrons = [

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.root" class="_gaps">
<div class="_gaps">
<div :class="$style.header">
<MkSelect v-model="type" :class="$style.typeSelect">
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
@@ -24,7 +24,7 @@
</button>
</div>
<div v-if="type === 'and' || type === 'or'" :class="$style.values" class="_gaps">
<div v-if="type === 'and' || type === 'or'" class="_gaps">
<Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5">
<template #item="{element}">
<div :class="$style.item">
@@ -118,10 +118,6 @@ function removeSelf() {
</script>
<style lang="scss" module>
.root {
}
.header {
display: flex;
}
@@ -148,8 +144,4 @@ function removeSelf() {
border-color: var(--accent);
}
}
.values {
}
</style>

View File

@@ -28,8 +28,8 @@
<template #icon><i class="ti ti-alert-triangle"></i></template>
<template #label>Errored instances</template>
<template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template>
<div :class="$style.jobs">
<div>
<div v-if="jobs.length > 0">
<div v-for="job in jobs" :key="job[0]">
<MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA>
@@ -150,7 +150,4 @@ onUnmounted(() => {
font-size: 80%;
opacity: 0.6;
}
.jobs {
}
</style>

View File

@@ -27,7 +27,7 @@
</Sortable>
<div :class="$style.commands">
<MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton primary rounded :class="$style.buttonSave" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</MkSpacer>

View File

@@ -3,7 +3,7 @@
<template #item="{element}">
<div :class="$style.item">
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
<component :is="'x-' + element.type" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/>
<component :is="getComponent(element.type)" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/>
</div>
</template>
</Sortable>
@@ -16,6 +16,16 @@ import XText from './els/page-editor.el.text.vue';
import XImage from './els/page-editor.el.image.vue';
import XNote from './els/page-editor.el.note.vue';
function getComponent(type: string) {
switch (type) {
case 'section': return XSection;
case 'text': return XText;
case 'image': return XImage;
case 'note': return XNote;
default: return null;
}
}
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const props = defineProps<{

View File

@@ -4,8 +4,8 @@
<template #label>{{ i18n.ts.usageAmount }}</template>
<div class="_gaps_m">
<div class="uawsfosz">
<div class="meter"><div :style="meterStyle"></div></div>
<div>
<div :class="$style.meter"><div :class="$style.meterValue" :style="meterStyle"></div></div>
</div>
<FormSplit>
<MkKeyValue>
@@ -139,22 +139,16 @@ definePageMetadata({
});
</script>
<style lang="scss" scoped>
<style lang="scss" module>
.meter {
height: 10px;
background: rgba(0, 0, 0, 0.1);
border-radius: 999px;
overflow: clip;
}
@use "sass:math";
.uawsfosz {
> .meter {
$size: 12px;
background: rgba(0, 0, 0, 0.1);
border-radius: math.div($size, 2);
overflow: hidden;
> div {
height: $size;
border-radius: math.div($size, 2);
}
}
.meterValue {
height: 100%;
border-radius: 999px;
}
</style>

View File

@@ -24,6 +24,7 @@
<div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
</div>
</FormSection>
@@ -147,7 +148,13 @@
<template #label>{{ i18n.ts.other }}</template>
<div class="_gaps">
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
<MkFolder>
<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
<div v-for="lang in emojiIndexLangs" class="_buttons">
<MkButton @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ lang }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</MkFolder>
<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
</div>
@@ -161,6 +168,8 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkRange from '@/components/MkRange.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/MkLink.vue';
@@ -253,6 +262,34 @@ watch([
await reloadAsk();
});
const emojiIndexLangs = ['en-US'];
function downloadEmojiIndex(lang: string) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
function download() {
switch (lang) {
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
default: throw new Error('unrecognized lang: ' + lang);
}
}
currentIndexes[lang] = await download();
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
}
function removeEmojiIndex(lang: string) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
delete currentIndexes[lang];
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);

View File

@@ -32,7 +32,7 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, useCssModule } from 'vue';
import { computed, onMounted, onUnmounted } from 'vue';
import { v4 as uuid } from 'uuid';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
@@ -48,8 +48,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { miLocalStorage } from '@/local-storage';
const { t, ts } = i18n;
useCssModule();
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'menu',
'visibility',

View File

@@ -3,7 +3,7 @@
<div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div :class="$style.avatarContainer">
<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
<MkButton primary rounded :class="$style.avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
</div>
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
</div>
@@ -271,10 +271,6 @@ definePageMetadata({
margin: 0 auto 16px auto;
}
.avatarEdit {
}
.bannerEdit {
position: absolute;
top: 16px;

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.root">
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()">
@@ -53,9 +53,6 @@ function submit() {
</script>
<style lang="scss" module>
.root {
}
.formContainer {
min-height: 100svh;
padding: 32px 32px 64px 32px;

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$style.root">
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()">
@@ -64,9 +64,6 @@ function submit() {
</script>
<style lang="scss" module>
.root {
}
.formContainer {
min-height: 100svh;
padding: 32px 32px 64px 32px;

View File

@@ -3,7 +3,7 @@
<div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]">
<div v-for="note in notes" :key="note.id" :class="$style.note">
<div class="_panel" :class="$style.content">
<div :class="$style.body">
<div>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>

View File

@@ -2,7 +2,6 @@ export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', '
export type UnicodeEmojiDef = {
name: string;
keywords: string[];
char: string;
category: typeof unicodeEmojiCategories[number];
}
@@ -10,11 +9,16 @@ export type UnicodeEmojiDef = {
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
import _emojilist from '../emojilist.json';
export const emojilist = _emojilist as UnicodeEmojiDef[];
export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
name: x[1] as string,
char: x[0] as string,
category: unicodeEmojiCategories[x[2]],
}));
const _indexByChar = new Map<string, number>();
const _charGroupByCategory = new Map<string, string[]>();
emojilist.forEach((emo, i) => {
for (let i = 0; i < emojilist.length; i++) {
const emo = emojilist[i];
_indexByChar.set(emo.char, i);
if (_charGroupByCategory.has(emo.category)) {
@@ -22,14 +26,14 @@ emojilist.forEach((emo, i) => {
} else {
_charGroupByCategory.set(emo.category, [emo.char]);
}
});
}
export const emojiCharByCategory = _charGroupByCategory;
export function getEmojiName(char: string): string | undefined {
export function getEmojiName(char: string): string | null {
const idx = _indexByChar.get(char);
if (typeof idx === 'undefined') {
return undefined;
if (idx == null) {
return null;
} else {
return emojilist[idx].name;
}

View File

@@ -73,7 +73,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
action: () => rename(file),
}, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off',
icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
action: () => toggleSensitive(file),
}, {
text: i18n.ts.describeFile,

View File

@@ -336,7 +336,11 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableCondensedLineForAcct: {
where: 'device',
default: true,
default: false,
},
additionalUnicodeEmojiIndexes: {
where: 'device',
default: {} as Record<string, Record<string, string[]>>,
},
}));

View File

@@ -22,11 +22,7 @@
}
html {
touch-action: manipulation;
background-color: var(--bg);
background-attachment: fixed;
background-size: cover;
background-position: center;
color: var(--fg);
accent-color: var(--accent);
overflow: auto;
@@ -38,7 +34,7 @@ html {
tab-size: 2;
&, * {
scrollbar-color: var(--scrollbarHandle) inherit;
scrollbar-color: var(--scrollbarHandle) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
@@ -87,6 +83,7 @@ html._themeChanging_ {
}
html, body {
touch-action: manipulation;
margin: 0;
padding: 0;
scroll-behavior: smooth;

View File

@@ -21,6 +21,7 @@
fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':lighten<3<@fg',
fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent',
panel: ':lighten<3<@bg',

View File

@@ -21,6 +21,7 @@
fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':darken<3<@fg',
fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent',
panel: ':lighten<3<@bg',

View File

@@ -53,6 +53,7 @@
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',
fgOnWhite: '@accent',
panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',

View File

@@ -11,6 +11,7 @@
bg: 'rgb(37, 38, 36)',
fg: 'rgb(216, 212, 199)',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: 'rgb(47, 47, 44)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -10,6 +10,7 @@
accent: 'rgb(255, 89, 117)',
bg: 'rgb(28, 28, 37)',
fg: 'rgb(236, 239, 244)',
fgOnWhite: '@accent',
panel: 'rgb(35, 35, 47)',
renote: '@accent',
link: '@accent',

View File

@@ -11,6 +11,7 @@
bg: '#232323',
fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: '#2d2d2d',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -12,6 +12,7 @@
fg: '#D5D5D6',
fgHighlighted: '#fff',
fgOnAccent: '#000',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.1)',
panel: '#18181c',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -12,6 +12,7 @@
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -12,6 +12,7 @@
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -8,6 +8,7 @@
props: {
accent: '#47BFE8',
fgOnWhite: '@accent',
bg: '#212526',
},
}

View File

@@ -11,6 +11,7 @@
bg: 'rgb(31, 33, 31)',
fg: '#cdd8c7',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: 'rgb(41, 43, 41)',
infoFg: '@fg',

View File

@@ -55,6 +55,7 @@
codeNumber: '#cfff9e',
codeString: '#ffb675',
fgOnAccent: '#fff',
fgOnWhite: '@accent',
infoWarnBg: '#42321c',
infoWarnFg: '#ffbd3e',
navHoverFg: ':lighten<17<@fg',

View File

@@ -10,6 +10,7 @@
accent: 'rgb(234, 154, 82)',
bg: '#e6e5e2',
fg: 'rgb(149, 143, 139)',
fgOnWhite: '@accent',
panel: '#EEECE8',
renote: '@accent',
link: '@accent',

View File

@@ -11,6 +11,7 @@
bg: 'e2deda',
fg: '#3d3d3d',
fgHighlighted: '#6bc9a0',
fgOnWhite: '@accent',
divider: '#cfcfcf',
panel: '@X14',
panelHeaderBg: '@panel',

View File

@@ -10,6 +10,7 @@
accent: 'rgb(219, 96, 114)',
bg: 'rgb(254, 248, 249)',
fg: 'rgb(152, 13, 26)',
fgOnWhite: '@accent',
panel: 'rgb(255, 255, 255)',
renote: '@accent',
link: 'rgb(156, 187, 5)',

View File

@@ -10,6 +10,7 @@
accent: '#9f8989',
bg: '#f5f3f3',
fg: '#7f6666',
fgOnWhite: '@accent',
panel: '#fff',
divider: 'rgba(87, 68, 68, 0.1)',
renote: 'rgb(160, 172, 125)',

View File

@@ -10,6 +10,7 @@
props: {
bg: '#f9f9f9',
fg: '#676767',
fgOnWhite: '@accent',
divider: '#e8e8e8',
header: ':alpha<0.7<@panel',
navBg: '#fff',

View File

@@ -10,6 +10,7 @@
accent: '#5db0da',
bg: 'rgb(246 248 249)',
fg: '#636b71',
fgOnWhite: '@accent',
panel: '#fff',
divider: 'rgb(230 233 234)',
panelHeaderDivider: '@divider',

View File

@@ -10,6 +10,7 @@
accent: '#e36749',
bg: '#f0eee9',
fg: '#5f5f5f',
fgOnWhite: '@accent',
renote: '@accent',
link: '@accent',
mention: '@accent',

View File

@@ -55,6 +55,7 @@
codeNumber: '#cfff9e',
codeString: '#ffb675',
fgOnAccent: '#fff',
fgOnWhite: '@accent',
infoWarnBg: '#42321c',
infoWarnFg: '#ffbd3e',
navHoverFg: ':lighten<17<@fg',

View File

@@ -52,6 +52,7 @@
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':darken<3<@fg',
fgTransparent: ':alpha<0.5<@fg',
fgOnWhite: '@accent',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',

View File

@@ -10,7 +10,15 @@
<XUpload v-if="uploads.length > 0"/>
<TransitionGroup
tag="div" :class="[$style.notifications, $style[`notificationsPosition-${defaultStore.state.notificationPosition}`], $style[`notificationsStackAxis-${defaultStore.state.notificationStackAxis}`]]"
tag="div"
:class="[$style.notifications, {
[$style.notificationsPosition_leftTop]: defaultStore.state.notificationPosition === 'leftTop',
[$style.notificationsPosition_leftBottom]: defaultStore.state.notificationPosition === 'leftBottom',
[$style.notificationsPosition_rightTop]: defaultStore.state.notificationPosition === 'rightTop',
[$style.notificationsPosition_rightBottom]: defaultStore.state.notificationPosition === 'rightBottom',
[$style.notificationsStackAxis_vertical]: defaultStore.state.notificationStackAxis === 'vertical',
[$style.notificationsStackAxis_horizontal]: defaultStore.state.notificationStackAxis === 'horizontal',
}]"
:moveClass="defaultStore.state.animation ? $style.transition_notification_move : ''"
:enterActiveClass="defaultStore.state.animation ? $style.transition_notification_enterActive : ''"
:leaveActiveClass="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''"
@@ -103,31 +111,31 @@ if ($i) {
pointer-events: none;
display: flex;
&.notificationsPosition-leftTop {
&.notificationsPosition_leftTop {
top: var(--margin);
left: 0;
}
&.notificationsPosition-rightTop {
&.notificationsPosition_rightTop {
top: var(--margin);
right: 0;
}
&.notificationsPosition-leftBottom {
&.notificationsPosition_leftBottom {
bottom: calc(var(--minBottomSpacing) + var(--margin));
left: 0;
}
&.notificationsPosition-rightBottom {
&.notificationsPosition_rightBottom {
bottom: calc(var(--minBottomSpacing) + var(--margin));
right: 0;
}
&.notificationsStackAxis-vertical {
&.notificationsStackAxis_vertical {
width: 250px;
&.notificationsPosition-leftTop,
&.notificationsPosition-rightTop {
&.notificationsPosition_leftTop,
&.notificationsPosition_rightTop {
flex-direction: column;
.notification {
@@ -137,8 +145,8 @@ if ($i) {
}
}
&.notificationsPosition-leftBottom,
&.notificationsPosition-rightBottom {
&.notificationsPosition_leftBottom,
&.notificationsPosition_rightBottom {
flex-direction: column-reverse;
.notification {
@@ -149,11 +157,11 @@ if ($i) {
}
}
&.notificationsStackAxis-horizontal {
&.notificationsStackAxis_horizontal {
width: 100%;
&.notificationsPosition-leftTop,
&.notificationsPosition-leftBottom {
&.notificationsPosition_leftTop,
&.notificationsPosition_leftBottom {
flex-direction: row;
.notification {
@@ -163,8 +171,8 @@ if ($i) {
}
}
&.notificationsPosition-rightTop,
&.notificationsPosition-rightBottom {
&.notificationsPosition_rightTop,
&.notificationsPosition_rightBottom {
flex-direction: row-reverse;
.notification {

View File

@@ -1,43 +1,41 @@
<template>
<div class="kmwsukvl">
<div class="body">
<div class="top">
<div class="banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
<button v-click-anime class="item _button instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
</button>
</div>
<div class="middle">
<MkA v-click-anime class="item index" activeClass="active" to="/" exact>
<i class="icon ti ti-home ti-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 class="item _button" :class="[item, { active: navbarItemDef[item].active }]" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" activeClass="active" to="/admin">
<i class="icon ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="icon ti ti-grid-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
</button>
<MkA v-click-anime class="item" activeClass="active" to="/settings">
<i class="icon ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
</MkA>
</div>
<div class="bottom">
<button class="item _button post" data-cy-open-post-form @click="os.post">
<i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span>
</button>
<button v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text _nowrap" :user="$i"/>
</button>
</div>
<div :class="$style.root">
<div :class="$style.top">
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
<button class="_button" :class="$style.instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
</button>
</div>
<div :class="$style.middle">
<MkA :class="$style.item" :activeClass="$style.active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" :class="$style.divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
</component>
</template>
<div :class="$style.divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" :class="$style.item" :activeClass="$style.active" to="/admin">
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
</MkA>
<button :class="$style.item" class="_button" @click="more">
<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
</button>
<MkA :class="$style.item" :activeClass="$style.active" to="/settings">
<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
</MkA>
</div>
<div :class="$style.bottom">
<button class="_button" :class="$style.post" data-cy-open-post-form @click="os.post">
<i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span>
</button>
<button class="_button" :class="$style.account" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" class="_nowrap" :user="$i"/>
</button>
</div>
</div>
</template>
@@ -73,192 +71,186 @@ function more() {
}
</script>
<style lang="scss" scoped>
.kmwsukvl {
> .body {
display: flex;
flex-direction: column;
<style lang="scss" module>
.root {
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));
.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%);
}
.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%;
.instance {
position: relative;
display: block;
text-align: center;
width: 100%;
}
> .icon {
display: inline-block;
width: 38px;
aspect-ratio: 1;
}
}
}
.instanceIcon {
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));
.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;
.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));
}
&: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;
width: 100%;
text-align: left;
box-sizing: border-box;
margin-top: 16px;
> .avatar {
display: block;
flex-shrink: 0;
position: relative;
width: 32px;
aspect-ratio: 1;
margin-right: 8px;
}
> .text {
display: block;
flex-shrink: 1;
padding-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);
}
}
}
&:hover, &.active {
&:before {
background: var(--accentLighten);
}
}
}
.postIcon {
position: relative;
margin-left: 30px;
margin-right: 8px;
width: 32px;
}
.account {
position: relative;
display: flex;
align-items: center;
padding-left: 30px;
width: 100%;
text-align: left;
box-sizing: border-box;
margin-top: 16px;
}
.avatar {
display: block;
flex-shrink: 0;
position: relative;
width: 32px;
aspect-ratio: 1;
margin-right: 8px;
}
.acct {
display: block;
flex-shrink: 1;
padding-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);
&: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);
}
}
}
.itemIcon {
position: relative;
width: 32px;
margin-right: 8px;
}
.itemIndicator {
position: absolute;
top: 0;
left: 20px;
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;
}
.itemText {
position: relative;
font-size: 0.9em;
}
</style>

View File

@@ -1,51 +1,50 @@
<template>
<div class="mvcprjjd" :class="{ iconOnly }">
<div class="body">
<div class="top">
<div class="banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
<button v-click-anime v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
<div :class="[$style.root, { [$style.iconOnly]: iconOnly }]">
<div :class="$style.body">
<div :class="$style.top">
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
</button>
</div>
<div class="middle">
<MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact>
<i class="icon ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
<div :class="$style.middle">
<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
<div v-if="item === '-'" :class="$style.divider"></div>
<component
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
v-click-anime
v-tooltip.noDelay.right="navbarItemDef[item].title"
class="item _button"
:class="[item, { active: navbarItemDef[item].active }]"
activeClass="active"
class="_button"
:class="[$style.item, { [$style.active]: navbarItemDef[item].active }]"
:activeClass="$style.active"
:to="navbarItemDef[item].to"
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
>
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.noDelay.right="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin">
<i class="icon ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
<div :class="$style.divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="icon ti ti-grid-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
<button class="_button" :class="$style.item" @click="more">
<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
</button>
<MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.settings" class="item" activeClass="active" to="/settings">
<i class="icon ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
</MkA>
</div>
<div class="bottom">
<button v-tooltip.noDelay.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post">
<i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span>
<div :class="$style.bottom">
<button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post">
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
</button>
<button v-click-anime v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text _nowrap" :user="$i"/>
<button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
</button>
</div>
</div>
@@ -99,374 +98,376 @@ function more(ev: MouseEvent) {
}
</script>
<style lang="scss" scoped>
.mvcprjjd {
$nav-width: 250px;
$nav-icon-only-width: 80px;
<style lang="scss" module>
.root {
--nav-width: 250px;
--nav-icon-only-width: 72px;
flex: 0 0 $nav-width;
width: $nav-width;
flex: 0 0 var(--nav-width);
width: var(--nav-width);
box-sizing: border-box;
}
> .body {
position: fixed;
.body {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
width: var(--nav-icon-only-width);
height: 100dvh;
box-sizing: border-box;
overflow: auto;
overflow-x: clip;
overscroll-behavior: contain;
background: var(--navBg);
contain: strict;
display: flex;
flex-direction: column;
}
.root:not(.iconOnly) {
.body {
width: var(--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;
z-index: 1001;
width: $nav-icon-only-width;
height: 100dvh;
box-sizing: border-box;
overflow: auto;
overflow-x: clip;
background: var(--navBg);
contain: strict;
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%;
}
.instanceIcon {
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);
}
}
}
.postIcon {
position: relative;
margin-left: 30px;
margin-right: 8px;
width: 32px;
}
.postText {
position: relative;
}
.account {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding-left: 30px;
width: 100%;
text-align: left;
box-sizing: border-box;
margin-top: 16px;
}
&:not(.iconOnly) {
> .body {
width: $nav-width;
.avatar {
display: block;
flex-shrink: 0;
position: relative;
width: 32px;
aspect-ratio: 1;
margin-right: 8px;
}
> .top {
position: sticky;
.acct {
display: block;
flex-shrink: 1;
padding-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);
&: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;
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;
left: 0;
right: 0;
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;
width: 100%;
text-align: left;
box-sizing: border-box;
margin-top: 16px;
> .avatar {
display: block;
flex-shrink: 0;
position: relative;
width: 32px;
aspect-ratio: 1;
margin-right: 8px;
}
> .text {
display: block;
flex-shrink: 1;
padding-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);
}
}
}
border-radius: 999px;
background: var(--accentedBg);
}
}
}
&.iconOnly {
flex: 0 0 $nav-icon-only-width;
width: $nav-icon-only-width;
.itemIcon {
position: relative;
width: 32px;
margin-right: 8px;
}
> .body {
width: $nav-icon-only-width;
.itemIndicator {
position: absolute;
top: 0;
left: 20px;
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;
}
> .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));
.itemText {
position: relative;
font-size: 0.9em;
}
}
> .instance {
display: block;
text-align: center;
width: 100%;
.root.iconOnly {
flex: 0 0 var(--nav-icon-only-width);
width: var(--nav-icon-only-width);
> .icon {
display: inline-block;
width: 30px;
aspect-ratio: 1;
}
}
}
.body {
width: var(--nav-icon-only-width);
}
> .bottom {
position: sticky;
bottom: 0;
padding: 20px 0;
background: var(--X14);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
.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));
}
> .post {
display: block;
position: relative;
width: 100%;
height: 52px;
margin-bottom: 16px;
text-align: center;
.instance {
display: block;
text-align: center;
width: 100%;
}
&: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));
}
.instanceIcon {
display: inline-block;
width: 30px;
aspect-ratio: 1;
}
&:hover, &.active {
&:before {
background: var(--accentLighten);
}
}
.bottom {
position: sticky;
bottom: 0;
padding: 20px 0;
background: var(--X14);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
> .icon {
position: relative;
color: var(--fgOnAccent);
}
.post {
display: block;
position: relative;
width: 100%;
height: 52px;
margin-bottom: 16px;
text-align: center;
> .text {
display: none;
}
}
&: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));
}
> .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;
}
}
}
&:hover, &.active {
&:before {
background: var(--accentLighten);
}
}
}
.postIcon {
position: relative;
color: var(--fgOnAccent);
}
.postText {
display: none;
}
.account {
display: block;
text-align: center;
width: 100%;
}
.avatar {
display: inline-block;
width: 38px;
aspect-ratio: 1;
}
.acct {
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;
&: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;
}
}
}
.itemIcon {
display: block;
margin: 0 auto;
opacity: 0.7;
}
.itemText {
display: none;
}
.itemIndicator {
position: absolute;
top: 6px;
left: 24px;
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;
}
}
</style>

View File

@@ -14,7 +14,7 @@
<MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace">
{{ instance.host }}
</MkA>
<span :class="$style.divider"></span>
<span></span>
</span>
</MarqueeText>
</Transition>

View File

@@ -4,7 +4,6 @@
v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black,
[$style.verySmall]: x.size === 'verySmall',
[$style.small]: x.size === 'small',
[$style.medium]: x.size === 'medium',
[$style.large]: x.size === 'large',
[$style.veryLarge]: x.size === 'veryLarge',
}]"

View File

@@ -2,8 +2,8 @@
<div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected">
<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div>
<div :class="$style.command" class="_buttons">
<MkButton :class="$style.commandButton" small primary @click="reload">{{ i18n.ts.reload }}</MkButton>
<MkButton :class="$style.commandButton" small>{{ i18n.ts.doNothing }}</MkButton>
<MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton>
<MkButton small>{{ i18n.ts.doNothing }}</MkButton>
</div>
</div>
</template>
@@ -54,7 +54,4 @@ onUnmounted(() => {
.command {
margin-top: 8px;
}
.commandButton {
}
</style>

View File

@@ -306,6 +306,7 @@ async function deleteProfile() {
display: flex;
overflow-x: auto;
overflow-y: clip;
overscroll-behavior: contain;
background: var(--deckBg);
&.center {

View File

@@ -1,6 +1,6 @@
<template>
<div
:class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.isStacked]: isStacked, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]"
:class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]"
@dragover.prevent.stop="onDragover"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
@@ -24,6 +24,9 @@
<template v-else><i class="ti ti-chevron-down"></i></template>
</button>
<span :class="$style.title"><slot name="header"></slot></span>
<svg viewBox="0 0 16 16" version="1.1" :class="$style.grabber">
<path fill="currentColor" d="M10 13a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm0-4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm-4 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm5-9a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>
</svg>
<button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button>
</header>
<div v-if="active" ref="body" :class="$style.body">
@@ -301,6 +304,7 @@ function onDrop(ev) {
&::-webkit-scrollbar-track {
background: transparent;
}
scrollbar-color: var(--scrollbarHandle) transparent;
}
}
@@ -313,6 +317,7 @@ function onDrop(ev) {
&::-webkit-scrollbar-track {
background: inherit;
}
scrollbar-color: var(--scrollbarHandle) transparent;
}
}
}
@@ -364,32 +369,32 @@ function onDrop(ev) {
z-index: 1;
width: var(--deckColumnHeaderHeight);
line-height: var(--deckColumnHeaderHeight);
color: var(--faceTextButton);
&:hover {
color: var(--faceTextButtonHover);
}
&:active {
color: var(--faceTextButtonActive);
}
}
.toggleActive {
margin-left: -16px;
}
.menu {
.grabber {
margin-left: auto;
margin-right: 10px;
padding: 8px 8px;
box-sizing: border-box;
height: var(--deckColumnHeaderHeight);
cursor: move;
user-select: none;
opacity: 0.5;
}
.menu {
margin-right: -16px;
}
.body {
height: calc(100% - var(--deckColumnHeaderHeight));
overflow-y: auto;
overflow-x: hidden; // Safari does not supports clip
overflow-x: clip;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
box-sizing: border-box;
container-type: size;
background-color: var(--bg);
@@ -397,5 +402,6 @@ function onDrop(ev) {
&::-webkit-scrollbar-track {
background: var(--panel);
}
scrollbar-color: var(--scrollbarHandle) var(--panel);
}
</style>

View File

@@ -1,19 +1,15 @@
<template>
<div :class="[$style.root, { [$style.withWallpaper]: wallpaper }]">
<div :class="$style.root">
<XSidebar v-if="!isMobile" :class="$style.sidebar"/>
<MkStickyContainer :class="$style.contents">
<MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu">
<template #header><XStatusBars :class="$style.statusbars"/></template>
<main style="min-width: 0;" @contextmenu.stop="onContextmenu">
<div :class="$style.content" style="container-type: inline-size;">
<RouterView/>
</div>
<div :class="$style.spacer"></div>
</main>
<RouterView/>
<div :class="$style.spacer"></div>
</MkStickyContainer>
<div v-if="isDesktop" ref="widgetsEl" :class="$style.widgets">
<XWidgets :marginTop="'var(--margin)'" @mounted="attachSticky"/>
<div v-if="isDesktop" :class="$style.widgets">
<XWidgets/>
</div>
<button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
@@ -84,10 +80,10 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, inject, Ref } from 'vue';
import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, shallowRef, Ref } from 'vue';
import XCommon from './_common_/common.vue';
import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
import { instanceName } from '@/config';
import { StickySidebar } from '@/scripts/sticky-sidebar';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import * as os from '@/os';
import { defaultStore } from '@/store';
@@ -99,6 +95,7 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
import { deviceKind } from '@/scripts/device-kind';
import { miLocalStorage } from '@/local-storage';
import { CURRENT_STICKY_BOTTOM } from '@/const';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
@@ -114,9 +111,9 @@ window.addEventListener('resize', () => {
});
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
const widgetsEl = $shallowRef<HTMLElement>();
const widgetsShowing = $ref(false);
const navFooter = $shallowRef<HTMLElement>();
const contents = shallowRef<InstanceType<typeof MkStickyContainer>>();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
@@ -140,8 +137,6 @@ mainRouter.on('change', () => {
drawerMenuShowing.value = false;
});
document.documentElement.style.overflowY = 'scroll';
if (window.innerWidth > 1024) {
const tempUI = miLocalStorage.getItem('ui_temp');
if (tempUI) {
@@ -197,19 +192,13 @@ const onContextmenu = (ev) => {
}], ev);
};
const attachSticky = (el) => {
const sticky = new StickySidebar(widgetsEl);
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
};
function top() {
window.scroll({ top: 0, behavior: 'smooth' });
contents.value.rootEl.scrollTo({
top: 0,
behavior: 'smooth',
});
}
const wallpaper = miLocalStorage.getItem('wallpaper') != null;
let navFooterHeight = $ref(0);
provide<Ref<number>>(CURRENT_STICKY_BOTTOM, $$(navFooterHeight));
@@ -275,28 +264,33 @@ $widgets-hide-threshold: 1090px;
}
.root {
min-height: 100dvh;
height: 100dvh;
overflow: clip;
contain: strict;
box-sizing: border-box;
display: flex;
}
.withWallpaper {
background: var(--wallpaperOverlay);
//backdrop-filter: var(--blur, blur(4px));
}
.sidebar {
border-right: solid 0.5px var(--divider);
}
.contents {
width: 100%;
flex: 1;
height: 100%;
min-width: 0;
overflow: auto;
overflow-y: scroll;
overscroll-behavior: contain;
background: var(--bg);
}
.widgets {
padding: 0 var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
width: 350px;
height: 100%;
box-sizing: border-box;
overflow: auto;
padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
border-left: solid 0.5px var(--divider);
background: var(--bg);
@@ -328,6 +322,7 @@ $widgets-hide-threshold: 1090px;
top: 0;
right: 0;
z-index: 1001;
width: 310px;
height: 100dvh;
padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box;

View File

@@ -1,6 +1,6 @@
<template>
<div :class="$style.root" :style="{ paddingTop: marginTop }">
<XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/>
<div>
<XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/>
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
@@ -11,7 +11,7 @@
let editMode = $ref(false);
</script>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { } from 'vue';
import XWidgets from '@/components/MkWidgets.vue';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
@@ -21,28 +21,16 @@ const props = withDefaults(defineProps<{
// left = place: leftだけを表示
// right = rightとnullを表示
place?: 'left' | null | 'right';
marginTop?: string;
}>(), {
place: null,
marginTop: '0',
});
const emit = defineEmits<{
(ev: 'mounted', el?: Element): void;
}>();
let rootEl = $shallowRef<HTMLDivElement>();
const widgets = $computed(() => {
if (props.place === null) return defaultStore.reactiveState.widgets.value;
if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left');
return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left');
});
onMounted(() => {
emit('mounted', rootEl);
});
function addWidget(widget) {
defaultStore.set('widgets', [{
...widget,
@@ -82,16 +70,6 @@ function updateWidgets(thisWidgets) {
</script>
<style lang="scss" module>
.root {
position: sticky;
height: min-content;
box-sizing: border-box;
}
.widgets {
width: 300px;
}
.edit {
width: 100%;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
<template>
<MkContainer :naked="widgetProps.transparent" :showHeader="false" data-cy-mkw-clock>
<div :class="[$style.root, $style[widgetProps.size]]">
<div
:class="[$style.root, {
[$style.small]: widgetProps.size === 'small',
[$style.medium]: widgetProps.size === 'medium',
[$style.large]: widgetProps.size === 'large',
}]"
>
<div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace" :class="[$style.label, $style.a]">{{ tzAbbrev }}</div>
<MkAnalogClock
:class="$style.clock"

View File

@@ -12,7 +12,7 @@
<Transition :name="$style.change" mode="default" appear>
<MarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse">
<span v-for="item in items" :key="item.link" :class="$style.item">
<a :class="$style.link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span>
<a :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span>
</span>
</MarqueeText>
</Transition>