merge: upstream

This commit is contained in:
Marie 2023-12-23 02:09:23 +01:00
commit 5db583a3eb
701 changed files with 50809 additions and 13660 deletions

View file

@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, defineAsyncComponent } from 'vue';
import { defineAsyncComponent, computed } from 'vue';
import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@ -91,7 +91,7 @@ withDefaults(defineProps<{
first: false,
});
const usePasswordLessLogin = $computed(() => $i?.usePasswordLessLogin ?? false);
const usePasswordLessLogin = computed(() => $i?.usePasswordLessLogin ?? false);
async function registerTOTP(): Promise<void> {
const auth = await os.authenticateDialog();

View file

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { defineAsyncComponent, ref, computed } from 'vue';
import type * as Misskey from 'misskey-js';
import FormSuspense from '@/components/form/suspense.vue';
import MkButton from '@/components/MkButton.vue';
@ -101,9 +101,9 @@ function switchAccountWithToken(token: string) {
login(token);
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.accounts,

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { defineAsyncComponent, ref, computed } from 'vue';
import FormLink from '@/components/form/link.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@ -40,9 +40,9 @@ function generateToken() {
}, 'closed');
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: 'API',

View file

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import FormPagination from '@/components/MkPagination.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@ -71,9 +71,9 @@ function revoke(token) {
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.installedApps,

View file

@ -0,0 +1,69 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
:class="[$style.root, { [$style.active]: active }]"
@click="emit('click')"
>
<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import { $i } from '@/account.js';
const props = defineProps<{
active?: boolean;
decoration: {
id: string;
url: string;
name: string;
roleIdsThatCanBeUsedThisDecoration: string[];
};
angle?: number;
flipH?: boolean;
offsetX?: number;
offsetY?: number;
}>();
const emit = defineEmits<{
(ev: 'click'): void;
}>();
</script>
<style lang="scss" module>
.root {
cursor: pointer;
padding: 16px 16px 28px 16px;
border: solid 2px var(--divider);
border-radius: 8px;
text-align: center;
font-size: 90%;
overflow: clip;
contain: content;
}
.active {
background-color: var(--accentedBg);
border-color: var(--accent);
}
.name {
position: relative;
z-index: 10;
font-weight: bold;
margin-bottom: 20px;
}
.lock {
position: absolute;
bottom: 12px;
right: 12px;
}
</style>

View file

@ -0,0 +1,154 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="cancel"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.avatarDecorations }}</template>
<div>
<MkSpacer :marginMin="20" :marginMax="28">
<div style="text-align: center;">
<div :class="$style.name">{{ decoration.name }}</div>
<MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="decorationsForPreview" forceShowDecoration/>
</div>
<div class="_gaps_s">
<MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`">
<template #label>{{ i18n.ts.angle }}</template>
</MkRange>
<MkRange v-model="offsetX" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>X {{ i18n.ts.position }}</template>
</MkRange>
<MkRange v-model="offsetY" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>Y {{ i18n.ts.position }}</template>
</MkRange>
<MkSwitch v-model="flipH">
<template #label>{{ i18n.ts.flip }}</template>
</MkSwitch>
</div>
</MkSpacer>
<div :class="$style.footer" class="_buttonsCenter">
<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
</div>
</div>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { shallowRef, ref, computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.js';
import MkRange from '@/components/MkRange.vue';
import { $i } from '@/account.js';
const props = defineProps<{
usingIndex: number | null;
decoration: {
id: string;
url: string;
name: string;
};
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
(ev: 'attach', payload: {
angle: number;
flipH: boolean;
offsetX: number;
offsetY: number;
}): void;
(ev: 'update', payload: {
angle: number;
flipH: boolean;
offsetX: number;
offsetY: number;
}): void;
(ev: 'detach'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
const offsetY = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetY : null) ?? 0);
const decorationsForPreview = computed(() => {
const decoration = {
id: props.decoration.id,
url: props.decoration.url,
angle: angle.value,
flipH: flipH.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
};
const decorations = [...$i.avatarDecorations];
if (props.usingIndex != null) {
decorations[props.usingIndex] = decoration;
} else {
decorations.push(decoration);
}
return decorations;
});
function cancel() {
dialog.value.close();
}
async function update() {
emit('update', {
angle: angle.value,
flipH: flipH.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
});
dialog.value.close();
}
async function attach() {
emit('attach', {
angle: angle.value,
flipH: flipH.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
});
dialog.value.close();
}
async function detach() {
emit('detach');
dialog.value.close();
}
</script>
<style lang="scss" module>
.name {
position: relative;
z-index: 10;
font-weight: bold;
margin-bottom: 28px;
}
.footer {
position: sticky;
bottom: 0;
left: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View file

@ -0,0 +1,152 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<div v-if="!loading" class="_gaps">
<MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
<MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/>
<div v-if="$i.avatarDecorations.length > 0" v-panel :class="$style.current" class="_gaps_s">
<div>{{ i18n.ts.inUse }}</div>
<div :class="$style.decorations">
<XDecoration
v-for="(avatarDecoration, i) in $i.avatarDecorations"
:decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)"
:angle="avatarDecoration.angle"
:flipH="avatarDecoration.flipH"
:offsetX="avatarDecoration.offsetX"
:offsetY="avatarDecoration.offsetY"
:active="true"
@click="openDecoration(avatarDecoration, i)"
/>
</div>
<MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
</div>
<div :class="$style.decorations">
<XDecoration
v-for="avatarDecoration in avatarDecorations"
:key="avatarDecoration.id"
:decoration="avatarDecoration"
@click="openDecoration(avatarDecoration)"
/>
</div>
</div>
<div v-else>
<MkLoading/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, defineAsyncComponent, computed } from 'vue';
import * as Misskey from 'misskey-js';
import XDecoration from './avatar-decoration.decoration.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import MkInfo from '@/components/MkInfo.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const loading = ref(true);
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
os.api('get-avatar-decorations').then(_avatarDecorations => {
avatarDecorations.value = _avatarDecorations;
loading.value = false;
});
function openDecoration(avatarDecoration, index?: number) {
os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), {
decoration: avatarDecoration,
usingIndex: index,
}, {
'attach': async (payload) => {
const decoration = {
id: avatarDecoration.id,
angle: payload.angle,
flipH: payload.flipH,
offsetX: payload.offsetX,
offsetY: payload.offsetY,
};
const update = [...$i.avatarDecorations, decoration];
await os.apiWithDialog('i/update', {
avatarDecorations: update,
});
$i.avatarDecorations = update;
},
'update': async (payload) => {
const decoration = {
id: avatarDecoration.id,
angle: payload.angle,
flipH: payload.flipH,
offsetX: payload.offsetX,
offsetY: payload.offsetY,
};
const update = [...$i.avatarDecorations];
update[index] = decoration;
await os.apiWithDialog('i/update', {
avatarDecorations: update,
});
$i.avatarDecorations = update;
},
'detach': async () => {
const update = [...$i.avatarDecorations];
update.splice(index, 1);
await os.apiWithDialog('i/update', {
avatarDecorations: update,
});
$i.avatarDecorations = update;
},
}, 'closed');
}
function detachAllDecorations() {
os.confirm({
type: 'warning',
text: i18n.ts.areYouSure,
}).then(async ({ canceled }) => {
if (canceled) return;
await os.apiWithDialog('i/update', {
avatarDecorations: [],
});
$i.avatarDecorations = [];
});
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.avatarDecorations,
icon: 'ti ti-sparkles',
});
</script>
<style lang="scss" module>
.avatar {
display: inline-block;
width: 72px;
height: 72px;
margin: 16px auto;
}
.current {
padding: 16px;
border-radius: var(--radius);
}
.decorations {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
grid-gap: 12px;
}
</style>

View file

@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo>
<MkTextarea v-model="localCustomCss" manualSave tall class="_monospace" style="tab-size: 2;">
<MkCodeEditor v-model="localCustomCss" manualSave lang="css">
<template #label>CSS</template>
</MkTextarea>
</MkCodeEditor>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { ref, watch, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import FormInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { unisonReload } from '@/scripts/unison-reload.js';
@ -41,9 +41,9 @@ watch(localCustomCss, async () => {
await apply();
});
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.customCss,

View file

@ -32,9 +32,9 @@ const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpl
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.deck,

View file

@ -55,12 +55,11 @@ import MkPagination from '@/components/MkPagination.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import { i18n } from '@/i18n.js';
import bytes from '@/filters/bytes.js';
import { dateString } from '@/filters/date.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkSelect from '@/components/MkSelect.vue';
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
let sortMode = ref('+size');
const sortMode = ref('+size');
const pagination = {
endpoint: 'drive/files' as const,
limit: 10,

View file

@ -72,7 +72,7 @@ const fetching = ref(true);
const usage = ref<any>(null);
const capacity = ref<any>(null);
const uploadFolder = ref<any>(null);
let alwaysMarkNsfw = $ref($i.alwaysMarkNsfw);
const alwaysMarkNsfw = ref($i.alwaysMarkNsfw);
const meterStyle = computed(() => {
return {
@ -117,20 +117,20 @@ function chooseUploadFolder() {
function saveProfile() {
os.api('i/update', {
alwaysMarkNsfw: !!alwaysMarkNsfw,
alwaysMarkNsfw: !!alwaysMarkNsfw.value,
}).catch(err => {
os.alert({
type: 'error',
title: i18n.ts.error,
text: err.message,
});
alwaysMarkNsfw = true;
alwaysMarkNsfw.value = true;
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.drive,

View file

@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { onMounted, ref, watch, computed } from 'vue';
import FormSection from '@/components/form/section.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkInput from '@/components/MkInput.vue';
@ -106,9 +106,9 @@ onMounted(() => {
});
});
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.email,

View file

@ -0,0 +1,274 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps_m">
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-pin"></i></template>
<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template>
<template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template>
<div class="_gaps">
<div>
<div v-panel style="border-radius: 6px;">
<Sortable
v-model="pinnedEmojisForReaction"
:class="$style.emojis"
:itemKey="item => item"
:animation="150"
:delay="100"
:delayOnTouchOnly="true"
>
<template #item="{element}">
<button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)">
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
<MkEmoji v-else :emoji="element" :normal="true"/>
</button>
</template>
<template #footer>
<button class="_button" :class="$style.emojisAdd" @click="chooseReaction">
<i class="ti ti-plus"></i>
</button>
</template>
</Sortable>
</div>
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
</div>
<div class="_buttons">
<MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
<MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton>
</div>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-pin"></i></template>
<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template>
<template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template>
<div class="_gaps">
<div>
<div v-panel style="border-radius: 6px;">
<Sortable
v-model="pinnedEmojis"
:class="$style.emojis"
:itemKey="item => item"
:animation="150"
:delay="100"
:delayOnTouchOnly="true"
>
<template #item="{element}">
<button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)">
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
<MkEmoji v-else :emoji="element" :normal="true"/>
</button>
</template>
<template #footer>
<button class="_button" :class="$style.emojisAdd" @click="chooseEmoji">
<i class="ti ti-plus"></i>
</button>
</template>
</Sortable>
</div>
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
</div>
<div class="_buttons">
<MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
<MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton>
</div>
</div>
</MkFolder>
<FormSection>
<template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
<div class="_gaps_m">
<MkRadios v-model="emojiPickerScale">
<template #label>{{ i18n.ts.size }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
</MkRadios>
<MkRadios v-model="emojiPickerWidth">
<template #label>{{ i18n.ts.numberOfColumn }}</template>
<option :value="1">5</option>
<option :value="2">6</option>
<option :value="3">7</option>
<option :value="4">8</option>
<option :value="5">9</option>
</MkRadios>
<MkRadios v-model="emojiPickerHeight">
<template #label>{{ i18n.ts.height }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
<option :value="4">{{ i18n.ts.large }}+</option>
</MkRadios>
<MkSwitch v-model="emojiPickerUseDrawerForMobile">
{{ i18n.ts.useDrawerReactionPickerForMobile }}
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
</MkSwitch>
</div>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, Ref, watch } from 'vue';
import Sortable from 'vuedraggable';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { deepClone } from '@/scripts/clone.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkFolder from '@/components/MkFolder.vue';
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions));
const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis));
const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile'));
const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);
const setDefaultReaction = () => setDefault(pinnedEmojisForReaction);
const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev);
const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
const setDefaultEmoji = () => setDefault(pinnedEmojis);
function previewReaction(ev: MouseEvent) {
reactionPicker.show(getHTMLElement(ev));
}
function previewEmoji(ev: MouseEvent) {
emojiPicker.show(getHTMLElement(ev));
}
async function overwriteFromPinnedEmojis() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.overwriteContentConfirm,
});
if (canceled) {
return;
}
pinnedEmojisForReaction.value = [...pinnedEmojis.value];
}
async function overwriteFromPinnedEmojisForReaction() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.overwriteContentConfirm,
});
if (canceled) {
return;
}
pinnedEmojis.value = [...pinnedEmojisForReaction.value];
}
function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.remove,
action: () => {
itemsRef.value = itemsRef.value.filter(x => x !== reaction);
},
}], getHTMLElement(ev));
}
async function setDefault(itemsRef: Ref<string[]>) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.resetAreYouSure,
});
if (canceled) return;
itemsRef.value = deepClone(defaultStore.def.reactions.default);
}
async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
os.pickEmoji(getHTMLElement(ev), {
showPinned: false,
}).then(it => {
const emoji = it as string;
if (!itemsRef.value.includes(emoji)) {
itemsRef.value.push(emoji);
}
});
}
function getHTMLElement(ev: MouseEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
watch(pinnedEmojisForReaction, () => {
defaultStore.set('reactions', pinnedEmojisForReaction.value);
}, {
deep: true,
});
watch(pinnedEmojis, () => {
defaultStore.set('pinnedEmojis', pinnedEmojis.value);
}, {
deep: true,
});
definePageMetadata({
title: i18n.ts.emojiPicker,
icon: 'ti ti-mood-happy',
});
</script>
<style lang="scss" module>
.tab {
margin: calc(var(--margin) / 2) 0;
padding: calc(var(--margin) / 2) 0;
background: var(--bg);
}
.emojis {
padding: 12px;
font-size: 1.1em;
}
.emojisItem {
display: inline-block;
padding: 8px;
cursor: move;
}
.emojisAdd {
display: inline-block;
padding: 8px;
}
.editorCaption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
}
</style>

View file

@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="sharkey"><i class="sk-icons sk-shark ph-bold" style="top: 2px;position: relative;"></i> Sharkey</option>
<option value="misskey"><i class="sk-icons sk-misskey ph-bold" style="top: 2px;position: relative;"></i> Misskey</option>
</MkRadios>
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
</div>
<MkSelect v-model="instanceTicker">
@ -136,7 +137,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
<MkSwitch v-model="enableDataSaverMode">{{ i18n.ts.dataSaver }}</MkSwitch>
<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
</div>
<div>
<MkRadios v-model="emojiStyle">
@ -187,6 +188,37 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.numberOfPageCache }}</template>
<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
</MkRange>
<MkFolder>
<template #label>{{ i18n.ts.dataSaver }}</template>
<div class="_gaps_m">
<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
<div class="_buttons">
<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
</div>
<div class="_gaps_m">
<MkSwitch v-model="dataSaver.media">
{{ i18n.ts._dataSaver._media.title }}
<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.avatar">
{{ i18n.ts._dataSaver._avatar.title }}
<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.urlPreview">
{{ i18n.ts._dataSaver._urlPreview.title }}
<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.code">
{{ i18n.ts._dataSaver._code.title }}
<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
</MkSwitch>
</div>
</div>
</MkFolder>
</div>
</FormSection>
@ -220,6 +252,7 @@ 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';
import MkInfo from '@/components/MkInfo.vue';
import { langs } from '@/config.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
@ -234,6 +267,7 @@ const lang = ref(miLocalStorage.getItem('lang'));
const fontSize = ref(miLocalStorage.getItem('fontSize'));
const cornerRadius = ref(miLocalStorage.getItem('cornerRadius'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
const dataSaver = ref(defaultStore.state.dataSaver);
async function reloadAsk() {
const { canceled } = await os.confirm({
@ -250,6 +284,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen'));
const showBots = computed(defaultStore.makeGetterSetter('tlWithBots'));
@ -267,7 +302,6 @@ const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('dis
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
const enableDataSaverMode = computed(defaultStore.makeGetterSetter('enableDataSaverMode'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
@ -289,10 +323,12 @@ const showTickerOnReplies = computed(defaultStore.makeGetterSetter('showTickerOn
const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign'));
const uncollapseCW = computed(defaultStore.makeGetterSetter('uncollapseCW'));
const expandLongNote = computed(defaultStore.makeGetterSetter('expandLongNote'));
const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
miLocalStorage.removeItem('locale');
miLocalStorage.removeItem('localeVersion');
});
watch(fontSize, () => {
@ -338,9 +374,11 @@ watch([
overridedDeviceKind,
mediaListWithOneImageAppearance,
reactionsDisplaySize,
limitWidthOfReaction,
highlightSensitiveMedia,
keepScreenOn,
disableStreamingTimeline,
enableSeasonalScreenEffect,
], async () => {
await reloadAsk();
});
@ -419,9 +457,31 @@ function testNotification(): void {
}, 300);
}
const headerActions = $computed(() => []);
function enableAllDataSaver() {
const g = { ...defaultStore.state.dataSaver };
const headerTabs = $computed(() => []);
Object.keys(g).forEach((key) => { g[key] = true; });
dataSaver.value = g;
}
function disableAllDataSaver() {
const g = { ...defaultStore.state.dataSaver };
Object.keys(g).forEach((key) => { g[key] = false; });
dataSaver.value = g;
}
watch(dataSaver, (to) => {
defaultStore.set('dataSaver', to);
}, {
deep: true,
});
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.general,

View file

@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
@ -233,9 +233,9 @@ const importAntennas = async (ev) => {
os.api('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.importAndExport,

View file

@ -32,13 +32,11 @@ import { i18n } from '@/i18n.js';
import MkInfo from '@/components/MkInfo.vue';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
import { signout, $i } from '@/account.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { clearCache } from '@/scripts/clear-cache.js';
import { instance } from '@/instance.js';
import { useRouter } from '@/router.js';
import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
const indexInfo = {
title: i18n.ts.settings,
@ -51,14 +49,14 @@ const childInfo = ref(null);
const router = useRouter();
let narrow = $ref(false);
const narrow = ref(false);
const NARROW_THRESHOLD = 600;
let currentPage = $computed(() => router.currentRef.value.child);
const currentPage = computed(() => router.currentRef.value.child);
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
const menuDef = computed(() => [{
@ -67,37 +65,37 @@ const menuDef = computed(() => [{
icon: 'ph-user ph-bold ph-lg',
text: i18n.ts.profile,
to: '/settings/profile',
active: currentPage?.route.name === 'profile',
active: currentPage.value?.route.name === 'profile',
}, {
icon: 'ph-lock ph-bold ph-lg-open',
text: i18n.ts.privacy,
to: '/settings/privacy',
active: currentPage?.route.name === 'privacy',
active: currentPage.value?.route.name === 'privacy',
}, {
icon: 'ph-smiley ph-bold ph-lg',
text: i18n.ts.reaction,
to: '/settings/reaction',
active: currentPage?.route.name === 'reaction',
text: i18n.ts.emojiPicker,
to: '/settings/emoji-picker',
active: currentPage.value?.route.name === 'emojiPicker',
}, {
icon: 'ph-cloud ph-bold ph-lg',
text: i18n.ts.drive,
to: '/settings/drive',
active: currentPage?.route.name === 'drive',
active: currentPage.value?.route.name === 'drive',
}, {
icon: 'ph-bell ph-bold ph-lg',
text: i18n.ts.notifications,
to: '/settings/notifications',
active: currentPage?.route.name === 'notifications',
active: currentPage.value?.route.name === 'notifications',
}, {
icon: 'ph-envelope ph-bold ph-lg',
text: i18n.ts.email,
to: '/settings/email',
active: currentPage?.route.name === 'email',
active: currentPage.value?.route.name === 'email',
}, {
icon: 'ph-lock ph-bold ph-lg',
text: i18n.ts.security,
to: '/settings/security',
active: currentPage?.route.name === 'security',
active: currentPage.value?.route.name === 'security',
}],
}, {
title: i18n.ts.clientSettings,
@ -105,32 +103,32 @@ const menuDef = computed(() => [{
icon: 'ph-faders ph-bold ph-lg',
text: i18n.ts.general,
to: '/settings/general',
active: currentPage?.route.name === 'general',
active: currentPage.value?.route.name === 'general',
}, {
icon: 'ph-palette ph-bold ph-lg',
text: i18n.ts.theme,
to: '/settings/theme',
active: currentPage?.route.name === 'theme',
active: currentPage.value?.route.name === 'theme',
}, {
icon: 'ph-list ph-bold ph-lg-2',
text: i18n.ts.navbar,
to: '/settings/navbar',
active: currentPage?.route.name === 'navbar',
active: currentPage.value?.route.name === 'navbar',
}, {
icon: 'ph-equals ph-bold ph-lg',
text: i18n.ts.statusbar,
to: '/settings/statusbar',
active: currentPage?.route.name === 'statusbar',
active: currentPage.value?.route.name === 'statusbar',
}, {
icon: 'ph-music-notes ph-bold ph-lg',
text: i18n.ts.sounds,
to: '/settings/sounds',
active: currentPage?.route.name === 'sounds',
active: currentPage.value?.route.name === 'sounds',
}, {
icon: 'ph-plug ph-bold ph-lg',
text: i18n.ts.plugins,
to: '/settings/plugin',
active: currentPage?.route.name === 'plugin',
active: currentPage.value?.route.name === 'plugin',
}],
}, {
title: i18n.ts.otherSettings,
@ -138,56 +136,50 @@ const menuDef = computed(() => [{
icon: 'ph-seal-check ph-bold ph-lg',
text: i18n.ts.roles,
to: '/settings/roles',
active: currentPage?.route.name === 'roles',
active: currentPage.value?.route.name === 'roles',
}, {
icon: 'ph-prohibit ph-bold ph-lg',
text: i18n.ts.muteAndBlock,
to: '/settings/mute-block',
active: currentPage?.route.name === 'mute-block',
active: currentPage.value?.route.name === 'mute-block',
}, {
icon: 'ph-key ph-bold ph-lg',
text: 'API',
to: '/settings/api',
active: currentPage?.route.name === 'api',
active: currentPage.value?.route.name === 'api',
}, {
icon: 'ph-webhooks-logo ph-bold ph-lg',
text: 'Webhook',
to: '/settings/webhook',
active: currentPage?.route.name === 'webhook',
active: currentPage.value?.route.name === 'webhook',
}, {
icon: 'ph-package ph-bold ph-lg',
text: i18n.ts.importAndExport,
to: '/settings/import-export',
active: currentPage?.route.name === 'import-export',
active: currentPage.value?.route.name === 'import-export',
}, {
icon: 'ph-airplane ph-bold ph-lg',
text: `${i18n.ts.accountMigration}`,
to: '/settings/migration',
active: currentPage?.route.name === 'migration',
active: currentPage.value?.route.name === 'migration',
}, {
icon: 'ph-dots-three ph-bold ph-lg',
text: i18n.ts.other,
to: '/settings/other',
active: currentPage?.route.name === 'other',
active: currentPage.value?.route.name === 'other',
}],
}, {
items: [{
icon: 'ph-floppy-disk ph-bold ph-lg',
text: i18n.ts.preferencesBackups,
to: '/settings/preferences-backups',
active: currentPage?.route.name === 'preferences-backups',
active: currentPage.value?.route.name === 'preferences-backups',
}, {
type: 'button',
icon: 'ph-trash ph-bold ph-lg',
text: i18n.ts.clearCache,
action: async () => {
os.waiting();
miLocalStorage.removeItem('locale');
miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt');
await fetchCustomEmojis(true);
unisonReload();
await clearCache();
},
}, {
type: 'button',
@ -205,23 +197,23 @@ const menuDef = computed(() => [{
}],
}]);
watch($$(narrow), () => {
watch(narrow, () => {
});
onMounted(() => {
ro.observe(el.value);
narrow = el.value.offsetWidth < NARROW_THRESHOLD;
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
if (!narrow && currentPage?.route.name == null) {
if (!narrow.value && currentPage.value?.route.name == null) {
router.replace('/settings/profile');
}
});
onActivated(() => {
narrow = el.value.offsetWidth < NARROW_THRESHOLD;
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
if (!narrow && currentPage?.route.name == null) {
if (!narrow.value && currentPage.value?.route.name == null) {
router.replace('/settings/profile');
}
});
@ -231,7 +223,7 @@ onUnmounted(() => {
});
watch(router.currentRef, (to) => {
if (to.route.name === 'settings' && to.child?.route.name == null && !narrow) {
if (to.route.name === 'settings' && to.child?.route.name == null && !narrow.value) {
router.replace('/settings/profile');
}
});
@ -243,12 +235,13 @@ provideMetadataReceiver((info) => {
childInfo.value = null;
} else {
childInfo.value = info;
INFO.value.needWideArea = info.value.needWideArea ?? undefined;
}
});
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(INFO);
// w 890

View file

@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ph-envelope ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
<XWordMute/>
<XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.hardWordMute }}</template>
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
</MkFolder>
<MkFolder>
@ -119,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref, computed } from 'vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue';
@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js';
import { infoImageUrl } from '@/instance.js';
import { $i } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue';
const renoteMutingPagination = {
@ -146,9 +154,9 @@ const blockingPagination = {
limit: 10,
};
let expandedRenoteMuteItems = $ref([]);
let expandedMuteItems = $ref([]);
let expandedBlockItems = $ref([]);
const expandedRenoteMuteItems = ref([]);
const expandedMuteItems = ref([]);
const expandedBlockItems = ref([]);
async function unrenoteMute(user, ev) {
os.popupMenu([{
@ -184,32 +192,40 @@ async function unblock(user, ev) {
}
async function toggleRenoteMuteItem(item) {
if (expandedRenoteMuteItems.includes(item.id)) {
expandedRenoteMuteItems = expandedRenoteMuteItems.filter(x => x !== item.id);
if (expandedRenoteMuteItems.value.includes(item.id)) {
expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id);
} else {
expandedRenoteMuteItems.push(item.id);
expandedRenoteMuteItems.value.push(item.id);
}
}
async function toggleMuteItem(item) {
if (expandedMuteItems.includes(item.id)) {
expandedMuteItems = expandedMuteItems.filter(x => x !== item.id);
if (expandedMuteItems.value.includes(item.id)) {
expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id);
} else {
expandedMuteItems.push(item.id);
expandedMuteItems.value.push(item.id);
}
}
async function toggleBlockItem(item) {
if (expandedBlockItems.includes(item.id)) {
expandedBlockItems = expandedBlockItems.filter(x => x !== item.id);
if (expandedBlockItems.value.includes(item.id)) {
expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id);
} else {
expandedBlockItems.push(item.id);
expandedBlockItems.value.push(item.id);
}
}
const headerActions = $computed(() => []);
async function saveMutedWords(mutedWords: (string | string[])[]) {
await os.api('i/update', { mutedWords });
}
const headerTabs = $computed(() => []);
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
await os.api('i/update', { hardMutedWords });
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.muteAndBlock,

View file

@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkTab from '@/components/MkTab.vue';
import * as os from '@/os.js';
import number from '@/filters/number.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const props = defineProps<{
muted: (string[] | string)[];
}>();
const emit = defineEmits<{
(ev: 'save', value: (string[] | string)[]): void;
}>();
const render = (mutedWords) => mutedWords.map(x => {
if (Array.isArray(x)) {
@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => {
}
}).join('\n');
const tab = ref('soft');
const mutedWords = ref(render($i!.mutedWords));
const mutedWords = ref(render(props.muted));
const changed = ref(false);
watch(mutedWords, () => {
@ -85,9 +85,7 @@ async function save() {
return;
}
await os.api('i/update', {
mutedWords: parsed,
});
emit('save', parsed);
changed.value = false;
}

View file

@ -57,7 +57,6 @@ import { defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { deepClone } from '@/scripts/clone.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@ -115,9 +114,9 @@ watch(menuDisplay, async () => {
await reloadAsk();
});
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.navbar,

View file

@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
@ -41,10 +41,10 @@ const emit = defineEmits<{
(ev: 'update', result: any): void;
}>();
let type = $ref(props.value.type);
let userListId = $ref(props.value.userListId);
const type = ref(props.value.type);
const userListId = ref(props.value.userListId);
function save() {
emit('update', { type, userListId });
emit('update', { type: type.value, userListId: userListId.value });
}
</script>

View file

@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { shallowRef, computed } from 'vue';
import XNotificationConfig from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
@ -68,11 +68,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { notificationTypes } from '@/const.js';
const nonConfigurableNotificationTypes = ['note'];
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned'];
let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadMessage || false);
const userLists = await os.api('users/lists/list');
async function readAllUnreadNotes() {
@ -95,14 +95,14 @@ async function updateReceiveConfig(type, value) {
}
function onChangeSendReadMessage(v: boolean) {
if (!pushRegistrationInServer) return;
if (!pushRegistrationInServer.value) return;
os.apiWithDialog('sw/update-registration', {
endpoint: pushRegistrationInServer.endpoint,
endpoint: pushRegistrationInServer.value.endpoint,
sendReadMessage: v,
}).then(res => {
if (!allowButton) return;
allowButton.pushRegistrationInServer = res;
if (!allowButton.value) return;
allowButton.value.pushRegistrationInServer = res;
});
}
@ -110,9 +110,9 @@ function testNotification(): void {
os.api('notifications/test-notification');
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.notifications,

View file

@ -188,9 +188,9 @@ watch([
await reloadAsk();
});
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.other,

View file

@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._plugin.installWarn }}</FormInfo>
<MkTextarea v-model="code" tall>
<MkCodeEditor v-model="code" lang="is">
<template #label>{{ i18n.ts.code }}</template>
</MkTextarea>
</MkCodeEditor>
<div>
<MkButton :disabled="code == null" primary inline @click="install"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton>
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { nextTick, ref } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { nextTick, ref, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
@ -49,9 +49,9 @@ async function install() {
}
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts._plugin.install,

View file

@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { nextTick, ref } from 'vue';
import { nextTick, ref, computed } from 'vue';
import FormLink from '@/components/form/link.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
@ -121,9 +121,9 @@ function changeActive(plugin, active) {
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.plugins,

View file

@ -54,22 +54,24 @@ import { miLocalStorage } from '@/local-storage.js';
const { t, ts } = i18n;
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'collapseRenotes',
'menu',
'visibility',
'localOnly',
'statusbars',
'widgets',
'tl',
'pinnedUserLists',
'overridedDeviceKind',
'serverDisconnectedBehavior',
'collapseRenotes',
'showNoteActionsOnlyHover',
'nsfw',
'highlightSensitiveMedia',
'animation',
'animatedMfm',
'advancedMfm',
'loadRawImages',
'imageNewTab',
'dataSaver',
'disableShowingAnimatedImages',
'emojiStyle',
'disableDrawer',
@ -81,18 +83,37 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'useReactionPickerForContextMenu',
'showGapBetweenNotesInTimeline',
'instanceTicker',
'reactionPickerSize',
'reactionPickerWidth',
'reactionPickerHeight',
'reactionPickerUseDrawerForMobile',
'emojiPickerScale',
'emojiPickerWidth',
'emojiPickerHeight',
'emojiPickerUseDrawerForMobile',
'defaultSideView',
'menuDisplay',
'reportError',
'squareAvatars',
'showAvatarDecorations',
'numberOfPageCache',
'showNoteActionsOnlyHover',
'showClipButtonInNoteFooter',
'reactionsDisplaySize',
'forceShowAds',
'numberOfReplies',
'aiChanMode',
'devMode',
'mediaListWithOneImageAppearance',
'notificationPosition',
'notificationStackAxis',
'enableCondensedLineForAcct',
'keepScreenOn',
'defaultWithReplies',
'disableStreamingTimeline',
'useGroupedNotifications',
'sound_masterVolume',
'sound_note',
'sound_noteMy',
'sound_notification',
'sound_antenna',
'sound_channel',
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme',
@ -395,7 +416,7 @@ function menu(ev: MouseEvent, profileId: string) {
icon: 'ph-download ph-bold ph-lg',
href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
download: `${profiles.value[profileId].name}.json`,
}, null, {
}, { type: 'divider' }, {
text: ts.rename,
icon: 'ph-textbox ph-bold ph-lg',
action: () => rename(profileId),
@ -403,7 +424,7 @@ function menu(ev: MouseEvent, profileId: string) {
text: ts._preferencesBackups.save,
icon: 'ph-floppy-disk ph-bold ph-lg',
action: () => save(profileId),
}, null, {
}, { type: 'divider' }, {
text: ts.delete,
icon: 'ph-trash ph-bold ph-lg',
action: () => deleteProfile(profileId),

View file

@ -13,12 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
</MkSwitch>
<MkSelect v-model="ffVisibility" @update:modelValue="save()">
<template #label>{{ i18n.ts.ffVisibility }}</template>
<MkSelect v-model="followingVisibility" @update:modelValue="save()">
<template #label>{{ i18n.ts.followingVisibility }}</template>
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
</MkSelect>
<MkSelect v-model="followersVisibility" @update:modelValue="save()">
<template #label>{{ i18n.ts.followersVisibility }}</template>
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
<template #caption>{{ i18n.ts.ffVisibilityDescription }}</template>
</MkSelect>
<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
@ -66,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
@ -77,36 +83,38 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
let isLocked = $ref($i.isLocked);
let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
let noCrawle = $ref($i.noCrawle);
let isExplorable = $ref($i.isExplorable);
let noindex = $ref($i.noindex);
let hideOnlineStatus = $ref($i.hideOnlineStatus);
let publicReactions = $ref($i.publicReactions);
let ffVisibility = $ref($i.ffVisibility);
const isLocked = ref($i.isLocked);
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle);
const noindex = ref($i.noindex);
const isExplorable = ref($i.isExplorable);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i?.followingVisibility);
const followersVisibility = ref($i?.followersVisibility);
let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
let rememberNoteVisibility = $computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
let keepCw = $computed(defaultStore.makeGetterSetter('keepCw'));
const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
function save() {
os.api('i/update', {
isLocked: !!isLocked,
autoAcceptFollowed: !!autoAcceptFollowed,
noCrawle: !!noCrawle,
isExplorable: !!isExplorable,
noindex: !!noindex,
hideOnlineStatus: !!hideOnlineStatus,
publicReactions: !!publicReactions,
ffVisibility: ffVisibility,
isLocked: !!isLocked.value,
autoAcceptFollowed: !!autoAcceptFollowed.value,
noCrawle: !!noCrawle.value,
noindex: !!noindex.value,
isExplorable: !!isExplorable.value,
hideOnlineStatus: !!hideOnlineStatus.value,
publicReactions: !!publicReactions.value,
followingVisibility: followingVisibility.value,
followersVisibility: followersVisibility.value,
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.privacy,

View file

@ -1,114 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="cancel"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.avatarDecorations }}</template>
<div>
<MkSpacer :marginMin="20" :marginMax="28">
<div style="text-align: center;">
<div :class="$style.name">{{ decoration.name }}</div>
<MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decoration="{ url: decoration.url, angle, flipH }" forceShowDecoration/>
</div>
<div class="_gaps_s">
<MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`">
<template #label>{{ i18n.ts.angle }}</template>
</MkRange>
<MkSwitch v-model="flipH">
<template #label>{{ i18n.ts.flip }}</template>
</MkSwitch>
</div>
</MkSpacer>
<div :class="$style.footer" class="_buttonsCenter">
<MkButton v-if="using" primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.update }}</MkButton>
<MkButton v-if="using" rounded @click="detach"><i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.detach }}</MkButton>
<MkButton v-else primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.attach }}</MkButton>
</div>
</div>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { shallowRef, ref, computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkRange from '@/components/MkRange.vue';
import { $i } from '@/account.js';
const props = defineProps<{
decoration: {
id: string;
url: string;
}
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const using = computed(() => $i.avatarDecorations.some(x => x.id === props.decoration.id));
const angle = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).angle ?? 0 : 0);
const flipH = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).flipH ?? false : false);
function cancel() {
dialog.value.close();
}
async function attach() {
const decoration = {
id: props.decoration.id,
angle: angle.value,
flipH: flipH.value,
};
await os.apiWithDialog('i/update', {
avatarDecorations: [decoration],
});
$i.avatarDecorations = [decoration];
dialog.value.close();
}
async function detach() {
await os.apiWithDialog('i/update', {
avatarDecorations: [],
});
$i.avatarDecorations = [];
dialog.value.close();
}
</script>
<style lang="scss" module>
.name {
position: relative;
z-index: 10;
font-weight: bold;
margin-bottom: 28px;
}
.footer {
position: sticky;
bottom: 0;
left: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View file

@ -5,20 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div class="_panel">
<div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
<MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">{{ i18n.ts._profile.changeBackground }}</MkButton>
</div>
<div :class="$style.avatarContainer">
<MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration @click="changeAvatar"/>
<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
<div class="_buttonsCenter">
<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
<MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ph-sparkle ph-bold ph-lg"></i></MkButton>
</div>
</div>
<MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">{{ i18n.ts._profile.changeBackground }}</MkButton>
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
</div>
<MkInput v-model="profile.name" :max="30" manualSave>
<MkInput v-model="profile.name" :max="30" manualSave :mfmAutocomplete="['emoji']">
<template #label>{{ i18n.ts._profile.name }}</template>
</MkInput>
<MkTextarea v-model="profile.description" :max="500" tall manualSave>
<MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts._profile.description }}</template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
@ -89,24 +94,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
</FormSlot>
<MkFolder>
<template #icon><i class="ph-sparkle ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts.avatarDecorations }}</template>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px;">
<div
v-for="avatarDecoration in avatarDecorations"
:key="avatarDecoration.id"
:class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
@click="openDecoration(avatarDecoration)"
>
<div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decoration="{ url: avatarDecoration.url }" forceShowDecoration/>
<i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ph-lock ph-bold ph-lg"></i>
</div>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.advancedSettings }}</template>
@ -129,10 +116,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
@ -147,11 +133,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import MkTextarea from '@/components/MkTextarea.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
let avatarDecorations: any[] = $ref([]);
const now = new Date();
@ -182,10 +168,6 @@ watch(() => profile, () => {
const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
const fieldEditMode = ref(false);
os.api('get-avatar-decorations').then(_avatarDecorations => {
avatarDecorations = _avatarDecorations;
});
function addField() {
fields.value.push({
id: Math.random().toString(),
@ -318,15 +300,9 @@ function changeBackground(ev) {
});
}
function openDecoration(avatarDecoration) {
os.popup(defineAsyncComponent(() => import('./profile.avatar-decoration-dialog.vue')), {
decoration: avatarDecoration,
}, {}, 'closed');
}
const headerActions = computed(() => []);
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.profile,
@ -335,19 +311,19 @@ definePageMetadata({
</script>
<style lang="scss" module>
.avatarAndBanner {
.banner {
position: relative;
height: 130px;
background-size: cover;
background-position: center;
border: solid 1px var(--divider);
border-radius: var(--radius);
border-bottom: solid 1px var(--divider);
overflow: clip;
}
.avatarContainer {
display: inline-block;
margin-top: -50px;
padding-bottom: 16px;
text-align: center;
padding: 16px;
}
.avatar {
@ -364,7 +340,7 @@ definePageMetadata({
}
.backgroundEdit {
position: absolute;
top: 103px;
top: 95px;
right: 16px;
}
@ -423,33 +399,4 @@ definePageMetadata({
.dragItemForm {
flex-grow: 1;
}
.avatarDecoration {
cursor: pointer;
padding: 16px 16px 28px 16px;
border: solid 2px var(--divider);
border-radius: var(--radius-sm);
text-align: center;
font-size: 90%;
overflow: clip;
contain: content;
}
.avatarDecorationActive {
background-color: var(--accentedBg);
border-color: var(--accent);
}
.avatarDecorationName {
position: relative;
z-index: 10;
font-weight: bold;
margin-bottom: 20px;
}
.avatarDecorationLock {
position: absolute;
bottom: 12px;
right: 12px;
}
</style>

View file

@ -1,196 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps_m">
<FromSlot>
<template #label>{{ i18n.ts.reactionSettingDescription }}</template>
<div v-panel style="border-radius: var(--radius-sm);">
<Sortable v-model="reactions" :class="$style.reactions" :itemKey="item => item" :animation="150" :delay="100" :delayOnTouchOnly="true">
<template #item="{element}">
<button class="_button" :class="$style.reactionsItem" @click="remove(element, $event)">
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
<MkEmoji v-else :emoji="element" :normal="true"/>
</button>
</template>
<template #footer>
<button class="_button" :class="$style.reactionsAdd" @click="chooseEmoji"><i class="ph-plus ph-bold ph-lg"></i></button>
</template>
</Sortable>
</div>
<template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template>
</FromSlot>
<FromSlot>
<template #label>{{ i18n.ts.defaultLike }}</template>
<MkCustomEmoji v-if="like && like.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="like" :normal="true" :noStyle="true"/>
<MkEmoji v-else-if="like && !like.startsWith(':')" :emoji="like" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/>
<span v-else-if="!like">{{ i18n.ts.notSet }}</span>
<div class="_buttons" style="padding-top: 8px;">
<MkButton rounded :small="true" inline @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton>
<MkButton rounded :small="true" inline @click="resetLike"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> Reset</MkButton>
</div>
</FromSlot>
<MkRadios v-model="reactionPickerSize">
<template #label>{{ i18n.ts.size }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
</MkRadios>
<MkRadios v-model="reactionPickerWidth">
<template #label>{{ i18n.ts.numberOfColumn }}</template>
<option :value="1">5</option>
<option :value="2">6</option>
<option :value="3">7</option>
<option :value="4">8</option>
<option :value="5">9</option>
</MkRadios>
<MkRadios v-model="reactionPickerHeight">
<template #label>{{ i18n.ts.height }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
<option :value="4">{{ i18n.ts.large }}+</option>
</MkRadios>
<MkSwitch v-model="reactionPickerUseDrawerForMobile">
{{ i18n.ts.useDrawerReactionPickerForMobile }}
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
</MkSwitch>
<FormSection>
<div class="_buttons">
<MkButton inline @click="preview"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton inline danger @click="setDefault"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton>
</div>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, watch } from 'vue';
import Sortable from 'vuedraggable';
import MkRadios from '@/components/MkRadios.vue';
import FromSlot from '@/components/form/slot.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { deepClone } from '@/scripts/clone.js';
import { unisonReload } from '@/scripts/unison-reload.js';
let reactions = $ref(deepClone(defaultStore.state.reactions));
const like = $computed(defaultStore.makeGetterSetter('like'));
const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight'));
const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'));
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
function save() {
defaultStore.set('reactions', reactions);
}
function remove(reaction, ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.remove,
action: () => {
reactions = reactions.filter(x => x !== reaction);
},
}], ev.currentTarget ?? ev.target);
}
function preview(ev: MouseEvent) {
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
asReactionPicker: true,
src: ev.currentTarget ?? ev.target,
}, {}, 'closed');
}
async function setDefault() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.resetAreYouSure,
});
if (canceled) return;
reactions = deepClone(defaultStore.def.reactions.default);
}
function chooseEmoji(ev: MouseEvent) {
os.pickEmoji(ev.currentTarget ?? ev.target, {
showPinned: false,
}).then(emoji => {
if (!reactions.includes(emoji)) {
reactions.push(emoji);
}
});
}
function chooseNewLike(ev: MouseEvent) {
os.pickEmoji(ev.currentTarget ?? ev.target, {
showPinned: false,
}).then(async emoji => {
defaultStore.set('like', emoji as string);
await reloadAsk();
});
}
async function resetLike() {
defaultStore.set('like', null);
await reloadAsk();
}
watch($$(reactions), () => {
save();
}, {
deep: true,
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.reaction,
icon: 'ph-smiley ph-bold ph-lg',
action: {
icon: 'ph-eye ph-bold ph-lg',
handler: preview,
},
});
</script>
<style lang="scss" module>
.reactions {
padding: 12px;
font-size: 1.1em;
}
.reactionsItem {
display: inline-block;
padding: 8px;
cursor: move;
}
.reactionsAdd {
display: inline-block;
padding: 8px;
}
</style>

View file

@ -23,21 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, reactive, watch } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import FormSlot from '@/components/form/slot.vue';
import { computed } from 'vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
function save() {
@ -46,9 +37,9 @@ function save() {
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.roles,

View file

@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import X2fa from './2fa.vue';
import FormSection from '@/components/form/section.vue';
import FormSlot from '@/components/form/slot.vue';
@ -97,9 +98,9 @@ async function regenerateToken() {
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.security,

View file

@ -7,8 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkSelect v-model="type">
<template #label>{{ i18n.ts.sound }}</template>
<option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option>
<option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option>
</MkSelect>
<div v-if="type === '_driveFile_'" :class="$style.fileSelectorRoot">
<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
<div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div>
</div>
<MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>{{ i18n.ts.volume }}</template>
</MkRange>
@ -21,30 +25,149 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref, computed } from 'vue';
import type { SoundType } from '@/scripts/sound.js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js';
import { playFile, soundsTypes } from '@/scripts/sound.js';
import * as os from '@/os.js';
import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
import { selectFile } from '@/scripts/select-file.js';
const props = defineProps<{
type: string;
type: SoundType;
fileId?: string;
fileUrl?: string;
volume: number;
}>();
const emit = defineEmits<{
(ev: 'update', result: { type: string; volume: number; }): void;
(ev: 'update', result: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }): void;
}>();
let type = $ref(props.type);
let volume = $ref(props.volume);
const type = ref<SoundType>(props.type);
const fileId = ref(props.fileId);
const fileUrl = ref(props.fileUrl);
const fileName = ref<string>('');
const volume = ref(props.volume);
if (type.value === '_driveFile_' && fileId.value) {
const apiRes = await os.api('drive/files/show', {
fileId: fileId.value,
});
fileName.value = apiRes.name;
}
function getSoundTypeName(f: SoundType): string {
switch (f) {
case null:
return i18n.ts.none;
case '_driveFile_':
return i18n.ts._soundSettings.driveFile;
default:
return f;
}
}
const friendlyFileName = computed<string>(() => {
if (fileName.value) {
return fileName.value;
}
if (fileUrl.value) {
return fileUrl.value;
}
return i18n.ts._soundSettings.driveFileWarn;
});
function selectSound(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts._soundSettings.driveFile).then(async (file) => {
if (!file.type.startsWith('audio')) {
os.alert({
type: 'warning',
title: i18n.ts._soundSettings.driveFileTypeWarn,
text: i18n.ts._soundSettings.driveFileTypeWarnDescription,
});
return;
}
const duration = await getSoundDuration(file.url);
if (duration >= 2000) {
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts._soundSettings.driveFileDurationWarn,
text: i18n.ts._soundSettings.driveFileDurationWarnDescription,
okText: i18n.ts.continue,
cancelText: i18n.ts.cancel,
});
if (canceled) return;
}
fileUrl.value = file.url;
fileName.value = file.name;
fileId.value = file.id;
});
}
function listen() {
playFile(type, volume);
if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) {
os.alert({
type: 'warning',
text: i18n.ts._soundSettings.driveFileWarn,
});
return;
}
playFile(type.value === '_driveFile_' ? {
type: '_driveFile_',
fileId: fileId.value as string,
fileUrl: fileUrl.value as string,
volume: volume.value,
} : {
type: type.value,
volume: volume.value,
});
}
function save() {
emit('update', { type, volume });
if (type.value === '_driveFile_' && !fileUrl.value) {
os.alert({
type: 'warning',
text: i18n.ts._soundSettings.driveFileWarn,
});
return;
}
if (type.value !== '_driveFile_') {
fileUrl.value = undefined;
fileName.value = '';
fileId.value = undefined;
}
emit('update', {
type: type.value,
fileId: fileId.value,
fileUrl: fileUrl.value,
volume: volume.value,
});
os.success();
}
</script>
<style module>
.fileSelectorRoot {
display: flex;
align-items: center;
gap: 8px;
}
.fileSelectorButton {
flex-shrink: 0;
}
.fileNotSelected {
font-weight: 700;
color: var(--infoWarnFg);
}
</style>

View file

@ -5,6 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkSwitch v-model="notUseSound">
<template #label>{{ i18n.ts.notUseSound }}</template>
</MkSwitch>
<MkSwitch v-model="useSoundOnlyWhenActive">
<template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template>
</MkSwitch>
<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>{{ i18n.ts.masterVolume }}</template>
</MkRange>
@ -12,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>{{ i18n.ts.sounds }}</template>
<div class="_gaps_s">
<MkFolder v-for="type in soundsKeys" :key="type">
<MkFolder v-for="type in operationTypes" :key="type">
<template #label>{{ i18n.t('_sfx.' + type) }}</template>
<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
<template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
</MkFolder>
</div>
</FormSection>
@ -27,6 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { Ref, computed, ref } from 'vue';
import type { SoundType, OperationType } from '@/scripts/sound.js';
import type { SoundStore } from '@/store.js';
import XSound from './sounds.sound.vue';
import MkRange from '@/components/MkRange.vue';
import MkButton from '@/components/MkButton.vue';
@ -34,23 +42,39 @@ import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { operationTypes } from '@/scripts/sound.js';
import { defaultStore } from '@/store.js';
import MkSwitch from '@/components/MkSwitch.vue';
const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const;
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
note: defaultStore.reactiveState.sound_note,
noteMy: defaultStore.reactiveState.sound_noteMy,
notification: defaultStore.reactiveState.sound_notification,
antenna: defaultStore.reactiveState.sound_antenna,
channel: defaultStore.reactiveState.sound_channel,
reaction: defaultStore.reactiveState.sound_reaction,
});
function getSoundTypeName(f: SoundType): string {
switch (f) {
case null:
return i18n.ts.none;
case '_driveFile_':
return i18n.ts._soundSettings.driveFile;
default:
return f;
}
}
async function updated(type: keyof typeof sounds.value, sound) {
const v = {
const v: SoundStore = {
type: sound.type,
fileId: sound.fileId,
fileUrl: sound.fileUrl,
volume: sound.volume,
};
@ -66,9 +90,9 @@ function reset() {
}
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.sounds,

View file

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { onMounted, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid';
import XStatusbar from './statusbar.statusbar.vue';
import MkFolder from '@/components/MkFolder.vue';
@ -27,11 +27,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const statusbars = defaultStore.reactiveState.statusbars;
let userLists = $ref();
const userLists = ref();
onMounted(() => {
os.api('users/lists/list').then(res => {
userLists = res;
userLists.value = res;
});
});
@ -45,9 +45,9 @@ async function add() {
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.statusbar,

View file

@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkTextarea v-model="installThemeCode">
<MkCodeEditor v-model="installThemeCode" lang="json5">
<template #label>{{ i18n.ts._theme.code }}</template>
</MkTextarea>
</MkCodeEditor>
<div class="_buttons">
<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton>
@ -17,15 +17,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { ref, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkButton from '@/components/MkButton.vue';
import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
let installThemeCode = $ref(null);
const installThemeCode = ref(null);
async function install(code: string): Promise<void> {
try {
@ -55,9 +55,9 @@ async function install(code: string): Promise<void> {
}
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts._theme.install,

View file

@ -72,9 +72,9 @@ function uninstall() {
os.success();
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts._theme.manage,

View file

@ -160,9 +160,9 @@ function setWallpaper(event) {
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.theme,

View file

@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -62,36 +62,36 @@ const webhook = await os.api('i/webhooks/show', {
webhookId: props.webhookId,
});
let name = $ref(webhook.name);
let url = $ref(webhook.url);
let secret = $ref(webhook.secret);
let active = $ref(webhook.active);
const name = ref(webhook.name);
const url = ref(webhook.url);
const secret = ref(webhook.secret);
const active = ref(webhook.active);
let event_follow = $ref(webhook.on.includes('follow'));
let event_followed = $ref(webhook.on.includes('followed'));
let event_note = $ref(webhook.on.includes('note'));
let event_reply = $ref(webhook.on.includes('reply'));
let event_renote = $ref(webhook.on.includes('renote'));
let event_reaction = $ref(webhook.on.includes('reaction'));
let event_mention = $ref(webhook.on.includes('mention'));
const event_follow = ref(webhook.on.includes('follow'));
const event_followed = ref(webhook.on.includes('followed'));
const event_note = ref(webhook.on.includes('note'));
const event_reply = ref(webhook.on.includes('reply'));
const event_renote = ref(webhook.on.includes('renote'));
const event_reaction = ref(webhook.on.includes('reaction'));
const event_mention = ref(webhook.on.includes('mention'));
async function save(): Promise<void> {
const events = [];
if (event_follow) events.push('follow');
if (event_followed) events.push('followed');
if (event_note) events.push('note');
if (event_reply) events.push('reply');
if (event_renote) events.push('renote');
if (event_reaction) events.push('reaction');
if (event_mention) events.push('mention');
if (event_follow.value) events.push('follow');
if (event_followed.value) events.push('followed');
if (event_note.value) events.push('note');
if (event_reply.value) events.push('reply');
if (event_renote.value) events.push('renote');
if (event_reaction.value) events.push('reaction');
if (event_mention.value) events.push('mention');
os.apiWithDialog('i/webhooks/update', {
name,
url,
secret,
name: name.value,
url: url.value,
secret: secret.value,
webhookId: props.webhookId,
on: events,
active,
active: active.value,
});
}
@ -109,9 +109,9 @@ async function del(): Promise<void> {
router.push('/settings/webhook');
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: 'Edit webhook',

View file

@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -48,39 +48,39 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
let name = $ref('');
let url = $ref('');
let secret = $ref('');
const name = ref('');
const url = ref('');
const secret = ref('');
let event_follow = $ref(true);
let event_followed = $ref(true);
let event_note = $ref(true);
let event_reply = $ref(true);
let event_renote = $ref(true);
let event_reaction = $ref(true);
let event_mention = $ref(true);
const event_follow = ref(true);
const event_followed = ref(true);
const event_note = ref(true);
const event_reply = ref(true);
const event_renote = ref(true);
const event_reaction = ref(true);
const event_mention = ref(true);
async function create(): Promise<void> {
const events = [];
if (event_follow) events.push('follow');
if (event_followed) events.push('followed');
if (event_note) events.push('note');
if (event_reply) events.push('reply');
if (event_renote) events.push('renote');
if (event_reaction) events.push('reaction');
if (event_mention) events.push('mention');
if (event_follow.value) events.push('follow');
if (event_followed.value) events.push('followed');
if (event_note.value) events.push('note');
if (event_reply.value) events.push('reply');
if (event_renote.value) events.push('renote');
if (event_reaction.value) events.push('reaction');
if (event_mention.value) events.push('mention');
os.apiWithDialog('i/webhooks/create', {
name,
url,
secret,
name: name.value,
url: url.value,
secret: secret.value,
on: events,
});
}
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: 'Create new webhook',

View file

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
@ -46,9 +46,9 @@ const pagination = {
noPaging: true,
};
const headerActions = $computed(() => []);
const headerActions = computed(() => []);
const headerTabs = $computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: 'Webhook',