summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages/settings/profile.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/pages/settings/profile.vue')
-rw-r--r--packages/frontend/src/pages/settings/profile.vue220
1 files changed, 220 insertions, 0 deletions
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
new file mode 100644
index 0000000000..14eeeaaa11
--- /dev/null
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -0,0 +1,220 @@
+<template>
+<div class="_formRoot">
+ <div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+ <div class="avatar">
+ <MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
+ <MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
+ </div>
+ <MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
+ </div>
+
+ <FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
+ <template #label>{{ i18n.ts._profile.name }}</template>
+ </FormInput>
+
+ <FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock">
+ <template #label>{{ i18n.ts._profile.description }}</template>
+ <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
+ </FormTextarea>
+
+ <FormInput v-model="profile.location" manual-save class="_formBlock">
+ <template #label>{{ i18n.ts.location }}</template>
+ <template #prefix><i class="ti ti-map-pin"></i></template>
+ </FormInput>
+
+ <FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock">
+ <template #label>{{ i18n.ts.birthday }}</template>
+ <template #prefix><i class="ti ti-cake"></i></template>
+ </FormInput>
+
+ <FormSelect v-model="profile.lang" class="_formBlock">
+ <template #label>{{ i18n.ts.language }}</template>
+ <option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
+ </FormSelect>
+
+ <FormSlot class="_formBlock">
+ <FormFolder>
+ <template #icon><i class="ti ti-list"></i></template>
+ <template #label>{{ i18n.ts._profile.metadataEdit }}</template>
+
+ <div class="_formRoot">
+ <FormSplit v-for="(record, i) in fields" :min-width="250" class="_formBlock">
+ <FormInput v-model="record.name" small>
+ <template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template>
+ </FormInput>
+ <FormInput v-model="record.value" small>
+ <template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template>
+ </FormInput>
+ </FormSplit>
+ <MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ <MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ </div>
+ </FormFolder>
+ <template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
+ </FormSlot>
+
+ <FormFolder>
+ <template #label>{{ i18n.ts.advancedSettings }}</template>
+
+ <div class="_formRoot">
+ <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
+ <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
+ </div>
+ </FormFolder>
+
+ <FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, watch } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import FormInput from '@/components/form/input.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormSplit from '@/components/form/split.vue';
+import FormFolder from '@/components/form/folder.vue';
+import FormSlot from '@/components/form/slot.vue';
+import { host } from '@/config';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
+import { langmap } from '@/scripts/langmap';
+import { definePageMetadata } from '@/scripts/page-metadata';
+
+const profile = reactive({
+ name: $i.name,
+ description: $i.description,
+ location: $i.location,
+ birthday: $i.birthday,
+ lang: $i.lang,
+ isBot: $i.isBot,
+ isCat: $i.isCat,
+ showTimelineReplies: $i.showTimelineReplies,
+});
+
+watch(() => profile, () => {
+ save();
+}, {
+ deep: true,
+});
+
+const fields = reactive($i.fields.map(field => ({ name: field.name, value: field.value })));
+
+function addField() {
+ fields.push({
+ name: '',
+ value: '',
+ });
+}
+
+while (fields.length < 4) {
+ addField();
+}
+
+function saveFields() {
+ os.apiWithDialog('i/update', {
+ fields: fields.filter(field => field.name !== '' && field.value !== ''),
+ });
+}
+
+function save() {
+ os.apiWithDialog('i/update', {
+ name: profile.name || null,
+ description: profile.description || null,
+ location: profile.location || null,
+ birthday: profile.birthday || null,
+ lang: profile.lang || null,
+ isBot: !!profile.isBot,
+ isCat: !!profile.isCat,
+ showTimelineReplies: !!profile.showTimelineReplies,
+ });
+}
+
+function changeAvatar(ev) {
+ selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => {
+ let originalOrCropped = file;
+
+ const { canceled } = await os.confirm({
+ type: 'question',
+ text: i18n.t('cropImageAsk'),
+ });
+
+ if (!canceled) {
+ originalOrCropped = await os.cropImage(file, {
+ aspectRatio: 1,
+ });
+ }
+
+ const i = await os.apiWithDialog('i/update', {
+ avatarId: originalOrCropped.id,
+ });
+ $i.avatarId = i.avatarId;
+ $i.avatarUrl = i.avatarUrl;
+ });
+}
+
+function changeBanner(ev) {
+ selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => {
+ let originalOrCropped = file;
+
+ const { canceled } = await os.confirm({
+ type: 'question',
+ text: i18n.t('cropImageAsk'),
+ });
+
+ if (!canceled) {
+ originalOrCropped = await os.cropImage(file, {
+ aspectRatio: 2,
+ });
+ }
+
+ const i = await os.apiWithDialog('i/update', {
+ bannerId: originalOrCropped.id,
+ });
+ $i.bannerId = i.bannerId;
+ $i.bannerUrl = i.bannerUrl;
+ });
+}
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.profile,
+ icon: 'ti ti-user',
+});
+</script>
+
+<style lang="scss" scoped>
+.llvierxe {
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ border: solid 1px var(--divider);
+ border-radius: 10px;
+ overflow: clip;
+
+ > .avatar {
+ display: inline-block;
+ text-align: center;
+ padding: 16px;
+
+ > .avatar {
+ display: inline-block;
+ width: 72px;
+ height: 72px;
+ margin: 0 auto 16px auto;
+ }
+ }
+
+ > .bannerEdit {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ }
+}
+</style>