Merge branch 'develop' into img-max

This commit is contained in:
tamaina
2023-04-12 02:09:36 +00:00
96 changed files with 1554 additions and 808 deletions

View File

@@ -77,6 +77,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
createdAt: '2016-12-28T22:49:51.000Z',
description: 'I am a cool user!',
ffVisibility: 'public',
roles: [],
fields: [
{
name: 'Website',

View File

@@ -398,6 +398,7 @@ function toStories(component: string): string {
Promise.all([
glob('src/components/global/*.vue'),
glob('src/components/MkGalleryPostPreview.vue'),
glob('src/pages/user/home.vue'),
])
.then((globs) => globs.flat())
.then((components) => Promise.all(components.map((component) => {

View File

@@ -0,0 +1,31 @@
<template>
<MkPagination :pagination="pagination">
<template #empty>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ i18n.ts.notFound }}</div>
</div>
</template>
<template #default="{ items }">
<MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/>
</template>
</MkPagination>
</template>
<script lang="ts" setup>
import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
pagination: Paging;
noGap?: boolean;
extractor?: (item: any) => any;
}>(), {
extractor: (item) => item,
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -82,6 +82,7 @@ export default defineComponent({
omitted: null,
ignoreOmit: false,
defaultStore,
i18n,
};
},
mounted() {

View File

@@ -439,7 +439,6 @@ defineExpose({
&.asDrawer {
width: 100% !important;
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
> .emojis {
::v-deep(section) {
@@ -498,6 +497,10 @@ defineExpose({
background: transparent;
color: var(--fg);
&:not(:focus):not(.filled) {
margin-bottom: env(safe-area-inset-bottom, 0px);
}
&:not(.filled) {
order: 1;
z-index: 2;

View File

@@ -31,7 +31,7 @@
import { onMounted } from 'vue';
import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import { ColdDeviceStorage } from '@/store';
import { soundConfigStore } from '@/scripts/sound';
import 'vue-plyr/dist/vue-plyr.css';
import { i18n } from '@/i18n';
@@ -44,11 +44,11 @@ const audioEl = $shallowRef<HTMLAudioElement | null>();
let hide = $ref(true);
function volumechange() {
if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume);
if (audioEl) soundConfigStore.set('mediaVolume', audioEl.volume);
}
onMounted(() => {
if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume');
if (audioEl) audioEl.volume = soundConfigStore.state.mediaVolume;
});
</script>

View File

@@ -1124,16 +1124,16 @@ defineExpose({
display: grid;
grid-auto-flow: row;
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
grid-auto-rows: 46px;
grid-auto-rows: 40px;
}
.footerRight {
flex: 0.3;
flex: 0;
margin-left: auto;
display: grid;
grid-auto-flow: row;
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
grid-auto-rows: 46px;
grid-auto-rows: 40px;
direction: rtl;
}
@@ -1198,13 +1198,21 @@ defineExpose({
}
}
@container (max-width: 330px) {
@container (max-width: 350px) {
.footer {
font-size: 0.9em;
}
.footerLeft {
grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
}
.footerRight {
grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
}
.headerRight {
gap: 0;
}
.footer {
font-size: 14px;
}
}
</style>

View File

@@ -83,7 +83,7 @@ const choseAd = (): Ad | null => {
};
const chosen = ref(choseAd());
const shouldHide = $ref($i && $i.policies.canHideAds && (props.specify == null));
const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
function reduceFrequency(): void {
if (chosen.value == null) return;

View File

@@ -5,7 +5,7 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { getStaticImageUrl } from '@/scripts/media-proxy';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy';
import { defaultStore } from '@/store';
import { customEmojis } from '@/custom-emojis';
@@ -15,25 +15,38 @@ const props = defineProps<{
noStyle?: boolean;
host?: string | null;
url?: string;
useOriginalSize?: boolean;
}>();
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substr(1, props.name.length - 2) : props.name).replace('@.', ''));
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
const rawUrl = computed(() => {
if (props.url) {
return props.url;
}
if (props.host == null && !customEmojiName.value.includes('@')) {
if (isLocal.value) {
return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
}
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
});
const url = computed(() =>
defaultStore.reactiveState.disableShowingAnimatedImages.value && rawUrl.value
? getStaticImageUrl(rawUrl.value)
: rawUrl.value,
);
const url = computed(() => {
if (rawUrl.value == null) return null;
const proxied =
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
? rawUrl.value
: getProxiedImageUrl(
rawUrl.value,
props.useOriginalSize ? undefined : 'emoji',
false,
true,
);
return defaultStore.reactiveState.disableShowingAnimatedImages.value
? getStaticImageUrl(proxied)
: proxied;
});
const alt = computed(() => `:${customEmojiName.value}:`);
let errored = $ref(url.value == null);

View File

@@ -51,6 +51,10 @@ export default defineComponent({
type: Object,
default: null,
},
rootScale: {
type: Number,
default: 1,
}
},
render() {
@@ -65,7 +69,12 @@ export default defineComponent({
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode | string | (VNode | string)[] => {
/**
* Gen Vue Elements from MFM AST
* @param ast MFM AST
* @param scale How times large the text is
*/
const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
switch (token.type) {
case 'text': {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
@@ -84,17 +93,17 @@ export default defineComponent({
}
case 'bold': {
return [h('b', genEl(token.children))];
return [h('b', genEl(token.children, scale))];
}
case 'strike': {
return [h('del', genEl(token.children))];
return [h('del', genEl(token.children, scale))];
}
case 'italic': {
return h('i', {
style: 'font-style: oblique;',
}, genEl(token.children));
}, genEl(token.children, scale));
}
case 'fn': {
@@ -155,17 +164,17 @@ export default defineComponent({
case 'x2': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
}, genEl(token.children));
}, genEl(token.children, scale * 2));
}
case 'x3': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
}, genEl(token.children));
}, genEl(token.children, scale * 3));
}
case 'x4': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
}, genEl(token.children));
}, genEl(token.children, scale * 4));
}
case 'font': {
const family =
@@ -182,7 +191,7 @@ export default defineComponent({
case 'blur': {
return h('span', {
class: '_mfm_blur_',
}, genEl(token.children));
}, genEl(token.children, scale));
}
case 'rainbow': {
const speed = validTime(token.props.args.speed) ?? '1s';
@@ -191,9 +200,9 @@ export default defineComponent({
}
case 'sparkle': {
if (!useAnim) {
return genEl(token.children);
return genEl(token.children, scale);
}
return h(MkSparkle, {}, genEl(token.children));
return h(MkSparkle, {}, genEl(token.children, scale));
}
case 'rotate': {
const degrees = parseFloat(token.props.args.deg ?? '90');
@@ -214,7 +223,8 @@ export default defineComponent({
}
const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
style = `transform: scale(${x}, ${y});`;
style = `transform: scale(${x}, ${y});`;
scale = scale * Math.max(x, y);
break;
}
case 'fg': {
@@ -231,24 +241,24 @@ export default defineComponent({
}
}
if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
} else {
return h('span', {
style: 'display: inline-block; ' + style,
}, genEl(token.children));
}, genEl(token.children, scale));
}
}
case 'small': {
return [h('small', {
style: 'opacity: 0.7;',
}, genEl(token.children))];
}, genEl(token.children, scale))];
}
case 'center': {
return [h('div', {
style: 'text-align:center;',
}, genEl(token.children))];
}, genEl(token.children, scale))];
}
case 'url': {
@@ -264,7 +274,7 @@ export default defineComponent({
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
}, genEl(token.children))];
}, genEl(token.children, scale))];
}
case 'mention': {
@@ -303,11 +313,11 @@ export default defineComponent({
if (!this.nowrap) {
return [h('div', {
style: QUOTE_STYLE,
}, genEl(token.children))];
}, genEl(token.children, scale))];
} else {
return [h('span', {
style: QUOTE_STYLE,
}, genEl(token.children))];
}, genEl(token.children, scale))];
}
}
@@ -319,6 +329,7 @@ export default defineComponent({
name: token.props.name,
normal: this.plain,
host: null,
useOriginalSize: scale >= 2.5,
})];
} else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -332,6 +343,7 @@ export default defineComponent({
url: this.emojiUrls ? this.emojiUrls[token.props.name] : null,
normal: this.plain,
host: this.author.host,
useOriginalSize: scale >= 2.5,
})];
}
}
@@ -360,7 +372,7 @@ export default defineComponent({
}
case 'plain': {
return [h('span', genEl(token.children))];
return [h('span', genEl(token.children, scale))];
}
default: {
@@ -373,6 +385,6 @@ export default defineComponent({
}).flat(Infinity) as (VNode | string)[];
// Parse ast to DOM
return h('span', genEl(ast));
return h('span', genEl(ast, this.rootScale));
},
});

View File

@@ -2,6 +2,23 @@
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div v-if="tab === 'search'">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkRadios v-model="searchType" @update:model-value="search()">
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
</MkRadios>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
<MkFoldableSection v-if="channelPagination">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkChannelList :key="key" :pagination="channelPagination"/>
</MkFoldableSection>
</div>
<div v-if="tab === 'featured'">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
@@ -28,17 +45,35 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkChannelList from '@/components/MkChannelList.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { useRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
const router = useRouter();
let tab = $ref('featured');
const props = defineProps<{
query: string;
type?: string;
}>();
let key = $ref('');
let tab = $ref('search');
let searchQuery = $ref('');
let searchType = $ref('nameAndDescription');
let channelPagination = $ref();
onMounted(() => {
searchQuery = props.query ?? '';
searchType = props.type ?? 'nameAndDescription';
});
const featuredPagination = {
endpoint: 'channels/featured' as const,
@@ -58,6 +93,25 @@ const ownedPagination = {
limit: 10,
};
async function search() {
const query = searchQuery.toString().trim();
if (query == null || query === '') return;
const type = searchType.toString().trim();
channelPagination = {
endpoint: 'channels/search',
limit: 10,
params: {
query: searchQuery,
type: type,
},
};
key = query + type;
}
function create() {
router.push('/channels/new');
}
@@ -69,6 +123,10 @@ const headerActions = $computed(() => [{
}]);
const headerTabs = $computed(() => [{
key: 'search',
title: i18n.ts.search,
icon: 'ti ti-search',
}, {
key: 'featured',
title: i18n.ts._channel.featured,
icon: 'ti ti-comet',

View File

@@ -66,7 +66,7 @@ const recentPostsPagination = {
};
const popularPostsPagination = {
endpoint: 'gallery/featured' as const,
limit: 5,
noPaging: true,
};
const myPostsPagination = {
endpoint: 'i/gallery/posts' as const,

View File

@@ -8,27 +8,29 @@
</div>
</template>
<template #default="{items}">
<div v-for="token in items" :key="token.id" class="_panel bfomjevm">
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
<div class="body">
<div class="name">{{ token.name }}</div>
<div class="description">{{ token.description }}</div>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.installedDate }}</template>
<template #value><MkTime :time="token.createdAt"/></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.lastUsedDate }}</template>
<template #value><MkTime :time="token.lastUsedAt"/></template>
</MkKeyValue>
<details>
<summary>{{ i18n.ts.details }}</summary>
<ul>
<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul>
</details>
<div class="actions">
<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
<div class="_gaps">
<div v-for="token in items" :key="token.id" class="_panel bfomjevm">
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
<div class="body">
<div class="name">{{ token.name }}</div>
<div class="description">{{ token.description }}</div>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.installedDate }}</template>
<template #value><MkTime :time="token.createdAt"/></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.lastUsedDate }}</template>
<template #value><MkTime :time="token.lastUsedAt"/></template>
</MkKeyValue>
<details>
<summary>{{ i18n.ts.details }}</summary>
<ul>
<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul>
</details>
<div class="actions">
<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
</div>
</div>
</div>
</div>
@@ -51,6 +53,7 @@ const list = ref<any>(null);
const pagination = {
endpoint: 'i/apps' as const,
limit: 100,
noPaging: true,
params: {
sort: '+lastUsedAt',
},

View File

@@ -61,6 +61,7 @@
<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
</div>
<div>
<MkRadios v-model="emojiStyle">
@@ -163,6 +164,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));

View File

@@ -400,7 +400,7 @@ function menu(ev: MouseEvent, profileId: string) {
icon: 'ti ti-device-floppy',
action: () => save(profileId),
}, null, {
text: ts._preferencesBackups.delete,
text: ts.delete,
icon: 'ti ti-trash',
action: () => deleteProfile(profileId),
danger: true,

View File

@@ -7,7 +7,7 @@
<FormSection>
<template #label>{{ i18n.ts.sounds }}</template>
<div class="_gaps_s">
<MkFolder v-for="type in Object.keys(sounds)" :key="type">
<MkFolder v-for="type in soundsKeys" :key="type">
<template #label>{{ i18n.t('_sfx.' + type) }}</template>
<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
@@ -21,51 +21,44 @@
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Ref, computed, ref } from 'vue';
import XSound from './sounds.sound.vue';
import MkRange from '@/components/MkRange.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import { ColdDeviceStorage } from '@/store';
import { soundConfigStore } from '@/scripts/sound';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const masterVolume = computed({
get: () => {
return ColdDeviceStorage.get('sound_masterVolume');
},
set: (value) => {
ColdDeviceStorage.set('sound_masterVolume', value);
},
const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume'));
const soundsKeys = ['note', 'noteMy', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const;
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
note: soundConfigStore.reactiveState.sound_note,
noteMy: soundConfigStore.reactiveState.sound_noteMy,
notification: soundConfigStore.reactiveState.sound_notification,
chat: soundConfigStore.reactiveState.sound_chat,
chatBg: soundConfigStore.reactiveState.sound_chatBg,
antenna: soundConfigStore.reactiveState.sound_antenna,
channel: soundConfigStore.reactiveState.sound_channel,
});
const volumeIcon = computed(() => masterVolume.value === 0 ? 'ti ti-volume-3' : 'ti ti-volume');
const sounds = ref({
note: ColdDeviceStorage.get('sound_note'),
noteMy: ColdDeviceStorage.get('sound_noteMy'),
notification: ColdDeviceStorage.get('sound_notification'),
chat: ColdDeviceStorage.get('sound_chat'),
chatBg: ColdDeviceStorage.get('sound_chatBg'),
antenna: ColdDeviceStorage.get('sound_antenna'),
channel: ColdDeviceStorage.get('sound_channel'),
});
async function updated(type, sound) {
async function updated(type: keyof typeof sounds.value, sound) {
const v = {
type: sound.type,
volume: sound.volume,
};
ColdDeviceStorage.set('sound_' + type, v);
soundConfigStore.set(`sound_${type}`, v);
sounds.value[type] = v;
}
function reset() {
for (const sound of Object.keys(sounds.value)) {
const v = ColdDeviceStorage.default['sound_' + sound];
ColdDeviceStorage.set('sound_' + sound, v);
for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
const v = soundConfigStore.def[`sound_${sound}`].default;
soundConfigStore.set(`sound_${sound}`, v);
sounds.value[sound] = v;
}
}

View File

@@ -7,18 +7,20 @@
<FormSection>
<MkPagination :pagination="pagination">
<template #default="{items}">
<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`" class="_margin">
<template #icon>
<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i>
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i>
</template>
{{ webhook.name || webhook.url }}
<template #suffix>
<MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime>
</template>
</FormLink>
<div class="_gaps">
<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`">
<template #icon>
<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i>
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i>
</template>
{{ webhook.name || webhook.url }}
<template #suffix>
<MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime>
</template>
</FormLink>
</div>
</template>
</MkPagination>
</FormSection>
@@ -35,7 +37,8 @@ import { i18n } from '@/i18n';
const pagination = {
endpoint: 'i/webhooks/list' as const,
limit: 10,
limit: 100,
noPaging: true,
};
const headerActions = $computed(() => []);

View File

@@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { userDetailed } from '../../../.storybook/fakes';
import { commonHandlers } from '../../../.storybook/mocks';
import home_ from './home.vue';
export const Default = {
render(args) {
return {
components: {
home_,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<home_ v-bind="props" />',
};
},
args: {
user: userDetailed(),
disableNotes: false,
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/notes', (req, res, ctx) => {
return res(ctx.json([]));
}),
rest.get('/api/charts/user/notes', (req, res, ctx) => {
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
return res(ctx.json({
total: Array.from({ length }, () => 0),
inc: Array.from({ length }, () => 0),
dec: Array.from({ length }, () => 0),
diffs: {
normal: Array.from({ length }, () => 0),
reply: Array.from({ length }, () => 0),
renote: Array.from({ length }, () => 0),
withFile: Array.from({ length }, () => 0),
},
}));
}),
rest.get('/api/charts/user/pv', (req, res, ctx) => {
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
return res(ctx.json({
upv: {
user: Array.from({ length }, () => 0),
visitor: Array.from({ length }, () => 0),
},
pv: {
user: Array.from({ length }, () => 0),
visitor: Array.from({ length }, () => 0),
},
}));
}),
],
},
chromatic: {
// `XActivity` is not compatible with Chromatic for now
disableSnapshot: true,
},
},
} satisfies StoryObj<typeof home_>;

View File

@@ -2,7 +2,7 @@ import { query } from '@/scripts/url';
import { url } from '@/config';
import { instance } from '@/instance';
export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigin: boolean = false): string {
export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin: boolean = false, noFallback: boolean = false): string {
const localProxy = `${url}/proxy`;
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
@@ -15,7 +15,7 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigi
: 'image.webp'
}?${query({
url: imageUrl,
fallback: '1',
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
})}`;

View File

@@ -1,4 +1,56 @@
import { ColdDeviceStorage } from '@/store';
import { markRaw } from 'vue';
import { Storage } from '@/pizzax';
export const soundConfigStore = markRaw(new Storage('sound', {
mediaVolume: {
where: 'device',
default: 0.5
},
sound_masterVolume: {
where: 'device',
default: 0.3
},
sound_note: {
where: 'account',
default: { type: 'syuilo/n-aec', volume: 1 }
},
sound_noteMy: {
where: 'account',
default: { type: 'syuilo/n-cea-4va', volume: 1 }
},
sound_notification: {
where: 'account',
default: { type: 'syuilo/n-ea', volume: 1 }
},
sound_chat: {
where: 'account',
default: { type: 'syuilo/pope1', volume: 1 }
},
sound_chatBg: {
where: 'account',
default: { type: 'syuilo/waon', volume: 1 }
},
sound_antenna: {
where: 'account',
default: { type: 'syuilo/triple', volume: 1 }
},
sound_channel: {
where: 'account',
default: { type: 'syuilo/square-pico', volume: 1 }
},
}));
await soundConfigStore.ready;
//#region サウンドのColdDeviceStorage => indexedDBのマイグレーション
for (const target of Object.keys(soundConfigStore.state) as Array<keyof typeof soundConfigStore.state>) {
const value = localStorage.getItem(`miux:${target}`);
if (value) {
soundConfigStore.set(target, JSON.parse(value) as typeof soundConfigStore.def[typeof target]['default']);
localStorage.removeItem(`miux:${target}`);
}
}
//#endregion
const cache = new Map<string, HTMLAudioElement>();
@@ -67,19 +119,20 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement {
}
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
const masterVolume = soundConfigStore.state.sound_masterVolume;
audio.volume = masterVolume - ((1 - volume) * masterVolume);
return audio;
}
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
const sound = ColdDeviceStorage.get(`sound_${type}`);
const sound = soundConfigStore.state[`sound_${type}`];
if (_DEV_) console.log('play', type, sound);
if (sound.type == null) return;
playFile(sound.type, sound.volume);
}
export function playFile(file: string, volume: number) {
const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
const masterVolume = soundConfigStore.state.sound_masterVolume;
if (masterVolume === 0) return;
const audio = setVolume(getAudio(file), volume);

View File

@@ -298,6 +298,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
forceShowAds: {
where: 'device',
default: false,
},
aiChanMode: {
where: 'device',
default: false,
@@ -343,15 +347,6 @@ export class ColdDeviceStorage {
darkTheme,
syncDeviceDarkMode: true,
plugins: [] as Plugin[],
mediaVolume: 0.5,
sound_masterVolume: 0.5,
sound_note: { type: 'syuilo/n-eca', volume: 0.5 },
sound_noteMy: { type: 'syuilo/n-cea-4va', volume: 0.5 },
sound_notification: { type: 'syuilo/n-ea', volume: 0.5 },
sound_chat: { type: 'syuilo/pope1', volume: 0.5 },
sound_chatBg: { type: 'syuilo/waon', volume: 0.5 },
sound_antenna: { type: 'syuilo/triple', volume: 0.5 },
sound_channel: { type: 'syuilo/square-pico', volume: 0.5 },
};
public static watchers: Watcher[] = [];

View File

@@ -1,17 +1,18 @@
import { post } from '@/os';
import { api, post } from '@/os';
import { $i, login } from '@/account';
import { getAccountFromId } from '@/scripts/get-account-from-id';
import { mainRouter } from '@/router';
import { deepClone } from '@/scripts/clone';
export function swInject() {
navigator.serviceWorker.addEventListener('message', ev => {
navigator.serviceWorker.addEventListener('message', async ev => {
if (_DEV_) {
console.log('sw msg', ev.data);
}
if (ev.data.type !== 'order') return;
if (ev.data.loginId !== $i?.id) {
if (ev.data.loginId && ev.data.loginId !== $i?.id) {
return getAccountFromId(ev.data.loginId).then(account => {
if (!account) return;
return login(account.token, ev.data.url);
@@ -19,8 +20,18 @@ export function swInject() {
}
switch (ev.data.order) {
case 'post':
return post(ev.data.options);
case 'post': {
const props = deepClone(ev.data.options);
// プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、
// 完全なノートを取得しなおす
if (props.reply) {
props.reply = await api('notes/show', { noteId: props.reply.id });
}
if (props.renote) {
props.renote = await api('notes/show', { noteId: props.renote.id });
}
return post(props);
}
case 'push':
if (mainRouter.currentRoute.value.path === ev.data.url) {
return window.scroll({ top: 0, behavior: 'smooth' });

View File

@@ -250,6 +250,7 @@ onMounted(() => {
> .widgets {
//--panelBorder: none;
width: 300px;
padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
@media (max-width: $widgets-hide-threshold) {
display: none;
@@ -304,7 +305,7 @@ onMounted(() => {
right: 0;
z-index: 1001;
height: 100dvh;
padding: var(--margin);
padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
overflow: auto;
background: var(--bg);

View File

@@ -296,7 +296,7 @@ $widgets-hide-threshold: 1090px;
}
.widgets {
padding: 0 var(--margin);
padding: 0 var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
border-left: solid 0.5px var(--divider);
background: var(--bg);
@@ -329,7 +329,7 @@ $widgets-hide-threshold: 1090px;
right: 0;
z-index: 1001;
height: 100dvh;
padding: var(--margin) !important;
padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;

View File

@@ -3,7 +3,7 @@
<XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
<button v-else class="_textButton mk-widget-edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
<button v-else class="_textButton mk-widget-edit" :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
</div>
</template>
@@ -91,4 +91,8 @@ function updateWidgets(thisWidgets) {
.widgets {
width: 300px;
}
.edit {
width: 100%;
}
</style>