Sharkey/packages/frontend/src/pages/channel-editor.vue
2024-02-19 10:47:42 +01:00

239 lines
6.6 KiB
Vue

<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700">
<div v-if="channelId == null || channel != null" class="_gaps_m">
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkTextarea v-model="description" mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<MkColorInput v-model="color">
<template #label>{{ i18n.ts.color }}</template>
</MkColorInput>
<MkSwitch v-model="isSensitive">
<template #label>{{ i18n.ts.sensitive }}</template>
</MkSwitch>
<MkSwitch v-model="allowRenoteToExternal">
<template #label>{{ i18n.ts._channel.allowRenoteToExternal }}</template>
</MkSwitch>
<div>
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
<div v-else-if="bannerUrl">
<img :src="bannerUrl" style="width: 100%;"/>
<MkButton @click="removeBannerImage()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts._channel.removeBanner }}</MkButton>
</div>
</div>
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.pinnedNotes }}</template>
<div class="_gaps">
<MkButton primary rounded @click="addPinnedNote()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
<Sortable
v-model="pinnedNotes"
itemKey="id"
:handle="'.' + $style.pinnedNoteHandle"
:animation="150"
>
<template #item="{element,index}">
<div :class="$style.pinnedNote">
<button class="_button" :class="$style.pinnedNoteHandle"><i class="ph-list ph-bold ph-lg"></i></button>
{{ element.id }}
<button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ph-x ph-bold ph-lg"></i></button>
</div>
</template>
</Sortable>
</div>
</MkFolder>
<div class="_buttons">
<MkButton primary @click="save()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton>
<MkButton v-if="channelId" danger @click="archive()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.archive }}</MkButton>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, ref, watch, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import { selectFile } from '@/scripts/select-file.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { useRouter } from '@/router/supplier.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const router = useRouter();
const props = defineProps<{
channelId?: string;
}>();
const channel = ref<Misskey.entities.Channel | null>(null);
const name = ref<string | null>(null);
const description = ref<string | null>(null);
const bannerUrl = ref<string | null>(null);
const bannerId = ref<string | null>(null);
const color = ref('#000');
const isSensitive = ref(false);
const allowRenoteToExternal = ref(true);
const pinnedNotes = ref<{ id: Misskey.entities.Note['id'] }[]>([]);
watch(() => bannerId.value, async () => {
if (bannerId.value == null) {
bannerUrl.value = null;
} else {
bannerUrl.value = (await misskeyApi('drive/files/show', {
fileId: bannerId.value,
})).url;
}
});
async function fetchChannel() {
if (props.channelId == null) return;
channel.value = await misskeyApi('channels/show', {
channelId: props.channelId,
});
name.value = channel.value.name;
description.value = channel.value.description;
bannerId.value = channel.value.bannerId;
bannerUrl.value = channel.value.bannerUrl;
isSensitive.value = channel.value.isSensitive;
pinnedNotes.value = channel.value.pinnedNoteIds.map(id => ({
id,
}));
color.value = channel.value.color;
allowRenoteToExternal.value = channel.value.allowRenoteToExternal;
}
fetchChannel();
async function addPinnedNote() {
const { canceled, result: value } = await os.inputText({
title: i18n.ts.noteIdOrUrl,
});
if (canceled) return;
const note = await os.apiWithDialog('notes/show', {
noteId: value.includes('/') ? value.split('/').pop() : value,
});
pinnedNotes.value = [{
id: note.id,
}, ...pinnedNotes.value];
}
function removePinnedNote(index: number) {
pinnedNotes.value.splice(index, 1);
}
function save() {
const params = {
name: name.value,
description: description.value,
bannerId: bannerId.value,
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
color: color.value,
isSensitive: isSensitive.value,
allowRenoteToExternal: allowRenoteToExternal.value,
};
if (props.channelId) {
params.channelId = props.channelId;
os.apiWithDialog('channels/update', params);
} else {
os.apiWithDialog('channels/create', params).then(created => {
router.push(`/channels/${created.id}`);
});
}
}
async function archive() {
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }),
text: i18n.ts.channelArchiveConfirmDescription,
});
if (canceled) return;
misskeyApi('channels/update', {
channelId: props.channelId,
isArchived: true,
}).then(() => {
os.success();
});
}
function setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
bannerId.value = file.id;
});
}
function removeBannerImage() {
bannerId.value = null;
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create,
icon: 'ph-television ph-bold ph-lg',
}));
</script>
<style lang="scss" module>
.pinnedNote {
position: relative;
display: block;
line-height: 2.85rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: var(--navFg);
}
.pinnedNoteRemove {
position: absolute;
z-index: 10000;
width: 32px;
height: 32px;
color: #ff2a2a;
right: 8px;
opacity: 0.8;
}
.pinnedNoteHandle {
cursor: move;
width: 32px;
height: 32px;
margin: 0 8px;
opacity: 0.5;
}
</style>