ae5d052274
to keep things manageable i merged a lot of one off values into just a handful of common sizes, so some parts of the ui will look different than upstream even with the "Misskey" rounding mode
290 lines
8.1 KiB
Vue
290 lines
8.1 KiB
Vue
<!--
|
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
-->
|
|
|
|
<template>
|
|
<MkStickyContainer>
|
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
|
<MkSpacer :contentMax="700" :class="$style.main">
|
|
<div v-if="channel && tab === 'overview'" class="_gaps">
|
|
<div class="_panel" :class="$style.bannerContainer">
|
|
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
|
|
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ph-star ph-bold pg-lg"></i></MkButton>
|
|
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ph-star ph-bold pg-lg"></i></MkButton>
|
|
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner">
|
|
<div :class="$style.bannerStatus">
|
|
<div><i class="ph-users ph-bold pg-lg ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
|
<div><i class="ph-pencil ph-bold ph-lg ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
|
</div>
|
|
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
|
<div :class="$style.bannerFade"></div>
|
|
</div>
|
|
<div v-if="channel.description" :class="$style.description">
|
|
<Mfm :text="channel.description" :isNote="false" :i="$i"/>
|
|
</div>
|
|
</div>
|
|
|
|
<MkFoldableSection>
|
|
<template #header><i class="ph-push-pin ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
|
|
<div v-if="channel.pinnedNotes.length > 0" class="_gaps">
|
|
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
|
|
</div>
|
|
</MkFoldableSection>
|
|
</div>
|
|
<div v-if="channel && tab === 'timeline'" class="_gaps">
|
|
<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
|
|
|
|
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
|
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
|
|
|
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after"/>
|
|
</div>
|
|
<div v-else-if="tab === 'featured'">
|
|
<MkNotes :pagination="featuredPagination"/>
|
|
</div>
|
|
<div v-else-if="tab === 'search'">
|
|
<div class="_gaps">
|
|
<div>
|
|
<MkInput v-model="searchQuery">
|
|
<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
|
|
</MkInput>
|
|
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
|
|
</div>
|
|
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
|
</div>
|
|
</div>
|
|
</MkSpacer>
|
|
<template #footer>
|
|
<div :class="$style.footer">
|
|
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
|
<div class="_buttonsCenter">
|
|
<MkButton inline rounded primary gradate @click="openPostForm()"><i class="ph-pencil ph-bold ph-lg"></i> {{ i18n.ts.postToTheChannel }}</MkButton>
|
|
</div>
|
|
</MkSpacer>
|
|
</div>
|
|
</template>
|
|
</MkStickyContainer>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, watch } from 'vue';
|
|
import MkPostForm from '@/components/MkPostForm.vue';
|
|
import MkTimeline from '@/components/MkTimeline.vue';
|
|
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
|
import * as os from '@/os.js';
|
|
import { useRouter } from '@/router.js';
|
|
import { $i, iAmModerator } from '@/account.js';
|
|
import { i18n } from '@/i18n.js';
|
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|
import { deviceKind } from '@/scripts/device-kind.js';
|
|
import MkNotes from '@/components/MkNotes.vue';
|
|
import { url } from '@/config.js';
|
|
import MkButton from '@/components/MkButton.vue';
|
|
import MkInput from '@/components/MkInput.vue';
|
|
import { defaultStore } from '@/store.js';
|
|
import MkNote from '@/components/MkNote.vue';
|
|
import MkInfo from '@/components/MkInfo.vue';
|
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
|
|
|
const router = useRouter();
|
|
|
|
const props = defineProps<{
|
|
channelId: string;
|
|
}>();
|
|
|
|
let tab = $ref('overview');
|
|
let channel = $ref(null);
|
|
let favorited = $ref(false);
|
|
let searchQuery = $ref('');
|
|
let searchPagination = $ref();
|
|
let searchKey = $ref('');
|
|
const featuredPagination = $computed(() => ({
|
|
endpoint: 'notes/featured' as const,
|
|
limit: 10,
|
|
params: {
|
|
channelId: props.channelId,
|
|
},
|
|
}));
|
|
|
|
watch(() => props.channelId, async () => {
|
|
channel = await os.api('channels/show', {
|
|
channelId: props.channelId,
|
|
});
|
|
favorited = channel.isFavorited;
|
|
if (favorited || channel.isFollowing) {
|
|
tab = 'timeline';
|
|
}
|
|
}, { immediate: true });
|
|
|
|
function edit() {
|
|
router.push(`/channels/${channel.id}/edit`);
|
|
}
|
|
|
|
function openPostForm() {
|
|
os.post({
|
|
channel,
|
|
});
|
|
}
|
|
|
|
function favorite() {
|
|
os.apiWithDialog('channels/favorite', {
|
|
channelId: channel.id,
|
|
}).then(() => {
|
|
favorited = true;
|
|
});
|
|
}
|
|
|
|
async function unfavorite() {
|
|
const confirm = await os.confirm({
|
|
type: 'warning',
|
|
text: i18n.ts.unfavoriteConfirm,
|
|
});
|
|
if (confirm.canceled) return;
|
|
os.apiWithDialog('channels/unfavorite', {
|
|
channelId: channel.id,
|
|
}).then(() => {
|
|
favorited = false;
|
|
});
|
|
}
|
|
|
|
async function search() {
|
|
const query = searchQuery.toString().trim();
|
|
|
|
if (query == null) return;
|
|
|
|
searchPagination = {
|
|
endpoint: 'notes/search',
|
|
limit: 10,
|
|
params: {
|
|
query: query,
|
|
channelId: channel.id,
|
|
},
|
|
};
|
|
|
|
searchKey = query;
|
|
}
|
|
|
|
const headerActions = $computed(() => {
|
|
if (channel && channel.userId) {
|
|
const share = {
|
|
icon: 'ph-share-network ph-bold pg-lg',
|
|
text: i18n.ts.share,
|
|
handler: async (): Promise<void> => {
|
|
navigator.share({
|
|
title: channel.name,
|
|
text: channel.description,
|
|
url: `${url}/channels/${channel.id}`,
|
|
});
|
|
},
|
|
};
|
|
|
|
const canEdit = ($i && $i.id === channel.userId) || iAmModerator;
|
|
return canEdit ? [share, {
|
|
icon: 'ph-gear ph-bold pg-lg',
|
|
text: i18n.ts.edit,
|
|
handler: edit,
|
|
}] : [share];
|
|
} else {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
const headerTabs = $computed(() => [{
|
|
key: 'overview',
|
|
title: i18n.ts.overview,
|
|
icon: 'ph-info ph-bold ph-lg',
|
|
}, {
|
|
key: 'timeline',
|
|
title: i18n.ts.timeline,
|
|
icon: 'ph-house ph-bold ph-lg',
|
|
}, {
|
|
key: 'featured',
|
|
title: i18n.ts.featured,
|
|
icon: 'ph-lightning ph-bold pg-lg',
|
|
}, {
|
|
key: 'search',
|
|
title: i18n.ts.search,
|
|
icon: 'ph-magnifying-glass ph-bold ph-lg',
|
|
}]);
|
|
|
|
definePageMetadata(computed(() => channel ? {
|
|
title: channel.name,
|
|
icon: 'ph-television ph-bold ph-lg',
|
|
} : null));
|
|
</script>
|
|
|
|
<style lang="scss" module>
|
|
.main {
|
|
min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
|
|
}
|
|
|
|
.footer {
|
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
|
backdrop-filter: var(--blur, blur(15px));
|
|
border-top: solid 0.5px var(--divider);
|
|
}
|
|
|
|
.bannerContainer {
|
|
position: relative;
|
|
}
|
|
|
|
.subscribe {
|
|
position: absolute;
|
|
z-index: 1;
|
|
top: 16px;
|
|
left: 16px;
|
|
}
|
|
|
|
.favorite {
|
|
position: absolute;
|
|
z-index: 1;
|
|
top: 16px;
|
|
right: 16px;
|
|
}
|
|
|
|
.banner {
|
|
position: relative;
|
|
height: 200px;
|
|
background-position: center;
|
|
background-size: cover;
|
|
}
|
|
|
|
.bannerFade {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 64px;
|
|
background: linear-gradient(0deg, var(--panel), var(--X15));
|
|
}
|
|
|
|
.bannerStatus {
|
|
position: absolute;
|
|
z-index: 1;
|
|
bottom: 16px;
|
|
right: 16px;
|
|
padding: 8px 12px;
|
|
font-size: 80%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
border-radius: var(--radius-sm);
|
|
color: #fff;
|
|
}
|
|
|
|
.description {
|
|
padding: 16px;
|
|
}
|
|
|
|
.sensitiveIndicator {
|
|
position: absolute;
|
|
z-index: 1;
|
|
bottom: 16px;
|
|
left: 16px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: var(--warn);
|
|
border-radius: var(--radius-sm);
|
|
font-weight: bold;
|
|
font-size: 1em;
|
|
padding: 4px 7px;
|
|
}
|
|
</style>
|