remove ads #5

Manually merged
Johann150 merged 4 commits from remove-ads into main 2022-07-13 21:27:40 +00:00
38 changed files with 17 additions and 678 deletions

View file

@ -734,7 +734,6 @@ gallery: "المعرض"
recentPosts: "المشاركات الحديثة"
popularPosts: "المشاركات المتداولة"
shareWithNote: "شاركه في ملاحظة"
ads: "الإعلانات"
expiration: "ينتهي استطلاع الرأي في"
memo: "تذكير"
priority: "الأولوية"

View file

@ -765,7 +765,6 @@ gallery: "গ্যালারী"
recentPosts: "নতুন পোস্ট"
popularPosts: "জনপ্রিয় পোস্ট"
shareWithNote: "নোটের মাধ্যমে শেয়ার করুন"
ads: "বিজ্ঞাপন"
expiration: "নির্দিষ্ট সময়সীমা"
memo: "মেমো"
priority: "অগ্রাধিকার"

View file

@ -766,7 +766,6 @@ gallery: "Galerie"
recentPosts: "Neue Beiträge"
popularPosts: "Beliebte Beiträge"
shareWithNote: "Mit Notiz teilen"
ads: "Werbung"
expiration: "Frist"
memo: "Merkzettel"
priority: "Priorität"

View file

@ -766,7 +766,6 @@ gallery: "Gallery"
recentPosts: "Recent posts"
popularPosts: "Popular posts"
shareWithNote: "Share with note"
ads: "Advertisements"
expiration: "Deadline"
memo: "Memo"
priority: "Priority"

View file

@ -761,7 +761,6 @@ gallery: "Galería"
recentPosts: "Posts recientes"
popularPosts: "Más vistos"
shareWithNote: "Compartir con una nota"
ads: "Anuncios"
expiration: "Termina el"
memo: "Notas"
priority: "Prioridad"

View file

@ -760,7 +760,6 @@ gallery: "Galerie"
recentPosts: "Les plus récentes"
popularPosts: "Les plus consultées"
shareWithNote: "Partager dans une note"
ads: "Publicité"
expiration: "Échéance"
memo: "Pense-bête"
priority: "Priorité"

View file

@ -765,7 +765,6 @@ gallery: "Galeri"
recentPosts: "Postingan terbaru"
popularPosts: "Postingan populer"
shareWithNote: "Bagikan dengan catatan"
ads: "Iklan"
expiration: "Batas akhir"
memo: "Memo"
priority: "Prioritas"

View file

@ -753,7 +753,6 @@ gallery: "Galleria"
recentPosts: "Le più recenti"
popularPosts: "Le più visualizzate"
shareWithNote: "Condividere in nota"
ads: "Pubblicità"
expiration: "Scadenza"
memo: "Promemoria"
priority: "Priorità"

View file

@ -768,7 +768,6 @@ gallery: "ギャラリー"
recentPosts: "最近の投稿"
popularPosts: "人気の投稿"
shareWithNote: "ノートで共有"
ads: "広告"
expiration: "期限"
memo: "メモ"
priority: "優先度"

View file

@ -645,7 +645,6 @@ goBack: "戻る"
info: "情報"
user: "ユーザー"
administration: "管理"
ads: "広告"
expiration: "期限"
memo: "メモ"
high: "高い"

View file

@ -765,7 +765,6 @@ gallery: "갤러리"
recentPosts: "최근 포스트"
popularPosts: "인기 포스트"
shareWithNote: "노트로 공유"
ads: "광고"
expiration: "기한"
memo: "메모"
priority: "우선순위"

View file

@ -739,7 +739,6 @@ gallery: "Galeria"
recentPosts: "Ostatnie wpisy"
popularPosts: "Popularne wpisy"
shareWithNote: "Udostępnij z wpisem"
ads: "Reklamy"
expiration: "Ankieta kończy się"
memo: "Notatki"
priority: "Priorytet"

View file

@ -763,7 +763,6 @@ gallery: "Галерея"
recentPosts: "Недавние публикации"
popularPosts: "Популярные публикации"
shareWithNote: "Поделиться заметкой"
ads: "Реклама"
expiration: "Опрос длится"
memo: "Памятка"
priority: "Приоритет"

View file

@ -764,7 +764,6 @@ gallery: "Galéria"
recentPosts: "Najnovšie príspevky"
popularPosts: "Populárne príspevky"
shareWithNote: "Zdieľať s poznámkou"
ads: "Reklamy"
expiration: "Ukončiť hlasovanie"
memo: "Memo"
priority: "Priorita"

View file

@ -765,7 +765,6 @@ gallery: "Thư viện ảnh"
recentPosts: "Tút gần đây"
popularPosts: "Tút được xem nhiều nhất"
shareWithNote: "Chia sẻ kèm với tút"
ads: "Quảng cáo"
expiration: "Thời hạn"
memo: "Lưu ý"
priority: "Ưu tiên"

View file

@ -765,7 +765,6 @@ gallery: "图库"
recentPosts: "最新发布"
popularPosts: "热门投稿"
shareWithNote: "在帖子中分享"
ads: "广告"
expiration: "截止时间"
memo: "便笺"
priority: "优先级"

View file

@ -765,7 +765,6 @@ gallery: "相簿"
recentPosts: "最新貼文"
popularPosts: "熱門的貼文"
shareWithNote: "在貼文中分享"
ads: "廣告"
expiration: "期限"
memo: "備忘錄"
priority: "優先級"

View file

@ -0,0 +1,12 @@
export class removeAds1657570176749 {
name = 'removeAds1657570176749'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "ad"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TABLE public.ad ("id" character varying(32) NOT NULL, "createdAt" timestamp with time zone NOT NULL, "expiresAt" timestamp with time zone NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, "ratio" integer DEFAULT 1 NOT NULL)`);
}
}

View file

@ -65,7 +65,6 @@ import { Channel } from '@/models/entities/channel.js';
import { ChannelFollowing } from '@/models/entities/channel-following.js';
import { ChannelNotePining } from '@/models/entities/channel-note-pining.js';
import { RegistryItem } from '@/models/entities/registry-item.js';
import { Ad } from '@/models/entities/ad.js';
import { PasswordResetRequest } from '@/models/entities/password-reset-request.js';
import { UserPending } from '@/models/entities/user-pending.js';
@ -168,7 +167,6 @@ export const entities = [
ChannelFollowing,
ChannelNotePining,
RegistryItem,
Ad,
PasswordResetRequest,
UserPending,
Webhook,

View file

@ -1,59 +0,0 @@
import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
import { id } from '../id.js';
@Entity()
export class Ad {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Ad.',
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The expired date of the Ad.',
})
public expiresAt: Date;
@Column('varchar', {
length: 32, nullable: false,
})
public place: string;
// 今は使われていないが将来的に活用される可能性はある
@Column('varchar', {
length: 32, nullable: false,
})
public priority: string;
@Column('integer', {
default: 1, nullable: false,
})
public ratio: number;
@Column('varchar', {
length: 1024, nullable: false,
})
public url: string;
@Column('varchar', {
length: 1024, nullable: false,
})
public imageUrl: string;
@Column('varchar', {
length: 8192, nullable: false,
})
public memo: string;
constructor(data: Partial<Ad>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View file

@ -59,7 +59,6 @@ import { MutedNote } from './entities/muted-note.js';
import { ChannelFollowing } from './entities/channel-following.js';
import { ChannelNotePining } from './entities/channel-note-pining.js';
import { RegistryItem } from './entities/registry-item.js';
import { Ad } from './entities/ad.js';
import { PasswordResetRequest } from './entities/password-reset-request.js';
import { UserPending } from './entities/user-pending.js';
import { InstanceRepository } from './repositories/instance.js';
@ -126,5 +125,4 @@ export const ChannelFollowings = db.getRepository(ChannelFollowing);
export const ChannelNotePinings = db.getRepository(ChannelNotePining);
export const RegistryItems = db.getRepository(RegistryItem);
export const Webhooks = db.getRepository(Webhook);
export const Ads = db.getRepository(Ad);
export const PasswordResetRequests = db.getRepository(PasswordResetRequest);

View file

@ -1,39 +0,0 @@
import define from '../../../define.js';
import { Ads } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
url: { type: 'string', minLength: 1 },
memo: { type: 'string' },
place: { type: 'string' },
priority: { type: 'string' },
ratio: { type: 'integer' },
expiresAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 },
},
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'imageUrl'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
await Ads.insert({
id: genId(),
createdAt: new Date(),
expiresAt: new Date(ps.expiresAt),
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,
ratio: ps.ratio,
place: ps.place,
memo: ps.memo,
});
});

View file

@ -1,35 +0,0 @@
import define from '../../../define.js';
import { Ads } from '@/models/index.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
errors: {
noSuchAd: {
message: 'No such ad.',
code: 'NO_SUCH_AD',
id: 'ccac9863-3a03-416e-b899-8a64041118b1',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
},
required: ['id'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const ad = await Ads.findOneBy({ id: ps.id });
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
await Ads.delete(ad.id);
});

View file

@ -1,30 +0,0 @@
import define from '../../../define.js';
import { Ads } from '@/models/index.js';
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId)
.andWhere('ad.expiresAt > :now', { now: new Date() });
const ads = await query.take(ps.limit).getMany();
return ads;
});

View file

@ -1,50 +0,0 @@
import define from '../../../define.js';
import { Ads } from '@/models/index.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
errors: {
noSuchAd: {
message: 'No such ad.',
code: 'NO_SUCH_AD',
id: 'b7aa1727-1354-47bc-a182-3a9c3973d300',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
memo: { type: 'string' },
url: { type: 'string', minLength: 1 },
imageUrl: { type: 'string', minLength: 1 },
place: { type: 'string' },
priority: { type: 'string' },
ratio: { type: 'integer' },
expiresAt: { type: 'integer' },
},
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const ad = await Ads.findOneBy({ id: ps.id });
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
await Ads.update(ad.id, {
url: ps.url,
place: ps.place,
priority: ps.priority,
ratio: ps.ratio,
memo: ps.memo,
imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt),
});
});

View file

@ -97,30 +97,6 @@ export const meta = {
},
},
},
ads: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
place: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
imageUrl: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
},
},
},
enableEmail: {
type: 'boolean',
optional: false, nullable: false,

View file

@ -1,7 +1,7 @@
import { IsNull, MoreThan } from 'typeorm';
import config from '@/config/index.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Ads, Emojis, Users } from '@/models/index.js';
import { Emojis, Users } from '@/models/index.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import define from '../define.js';
@ -168,30 +168,6 @@ export const meta = {
},
},
},
ads: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
place: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
imageUrl: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
},
},
},
requireSetup: {
type: 'boolean',
optional: false, nullable: false,
@ -310,12 +286,6 @@ export default define(meta, paramDef, async (ps, me) => {
},
});
const ads = await Ads.find({
where: {
expiresAt: MoreThan(new Date()),
},
});
const response: any = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
@ -349,13 +319,6 @@ export default define(meta, paramDef, async (ps, me) => {
emojis: await Emojis.packMany(emojis),
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
place: ad.place,
ratio: ad.ratio,
imageUrl: ad.imageUrl,
})),
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,

View file

@ -1,13 +1,12 @@
<script lang="ts">
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
import MkAd from '@/components/global/ad.vue';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
export default defineComponent({
props: {
items: {
type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
type: Array as PropType<{ id: string; createdAt: string; }[]>,
required: true,
},
direction: {
@ -25,11 +24,6 @@ export default defineComponent({
required: false,
default: false
},
ad: {
type: Boolean,
required: false,
default: false
},
},
setup(props, { slots, expose }) {
@ -78,15 +72,7 @@ export default defineComponent({
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
class: 'a', // advertise()
key: item.id + ':ad',
prefer: ['horizontal', 'horizontal-big'],
}), el];
} else {
return el;
}
return el;
}
});

View file

@ -1,200 +0,0 @@
<template>
<div v-if="ad" class="qiivuoyo">
<div v-if="!showMenu" class="main" :class="ad.place">
<a :href="ad.url" target="_blank">
<img :src="ad.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
</a>
</div>
<div v-else class="menu">
<div class="body">
<div>Ads by {{ host }}</div>
<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
<MkButton v-if="ad.ratio !== 0" class="button" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
</div>
</div>
</div>
<div v-else></div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { instance } from '@/instance';
import { host } from '@/config';
import MkButton from '@/components/ui/button.vue';
import { defaultStore } from '@/store';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton
},
props: {
prefer: {
type: Array,
required: true
},
specify: {
type: Object,
required: false
},
},
setup(props) {
const showMenu = ref(false);
const toggleMenu = () => {
showMenu.value = !showMenu.value;
};
const choseAd = (): (typeof instance)['ads'][number] | null => {
if (props.specify) {
return props.specify as (typeof instance)['ads'][number];
}
const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
...ad,
ratio: 0
} : ad);
let ads = allAds.filter(ad => props.prefer.includes(ad.place));
if (ads.length === 0) {
ads = allAds.filter(ad => ad.place === 'square');
}
const lowPriorityAds = ads.filter(ad => ad.ratio === 0);
ads = ads.filter(ad => ad.ratio !== 0);
if (ads.length === 0) {
if (lowPriorityAds.length !== 0) {
return lowPriorityAds[Math.floor(Math.random() * lowPriorityAds.length)];
} else {
return null;
}
}
const totalFactor = ads.reduce((a, b) => a + b.ratio, 0);
const r = Math.random() * totalFactor;
let stackedFactor = 0;
for (const ad of ads) {
if (r >= stackedFactor && r <= stackedFactor + ad.ratio) {
return ad;
} else {
stackedFactor += ad.ratio;
}
}
return null;
};
const chosen = ref(choseAd());
const reduceFrequency = () => {
if (chosen.value == null) return;
if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
defaultStore.push('mutedAds', chosen.value.id);
os.success();
chosen.value = choseAd();
showMenu.value = false;
};
return {
ad: chosen,
showMenu,
toggleMenu,
host,
reduceFrequency,
};
}
});
</script>
<style lang="scss" scoped>
.qiivuoyo {
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
> .main {
text-align: center;
> a {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> img {
filter: contrast(120%);
}
}
> img {
display: block;
object-fit: contain;
margin: auto;
}
> .menu {
position: absolute;
top: 0;
right: 0;
background: var(--panel);
}
}
&.square {
> a ,
> a > img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
&.horizontal {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
&.horizontal-big {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.vertical {
> a ,
> a > img {
max-width: min(100px, 100%);
}
}
}
> .menu {
padding: 8px;
text-align: center;
> .body {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
> .button {
margin: 8px auto;
}
}
}
}
</style>

View file

@ -13,7 +13,6 @@ import I18n from './global/i18n';
import RouterView from './global/router-view.vue';
import MkLoading from './global/loading.vue';
import MkError from './global/error.vue';
import MkAd from './global/ad.vue';
import MkPageHeader from './global/page-header.vue';
import MkSpacer from './global/spacer.vue';
import MkStickyContainer from './global/sticky-container.vue';
@ -32,7 +31,6 @@ export default function(app: App) {
app.component('MkUrl', MkUrl);
app.component('MkLoading', MkLoading);
app.component('MkError', MkError);
app.component('MkAd', MkAd);
app.component('MkPageHeader', MkPageHeader);
app.component('MkSpacer', MkSpacer);
app.component('MkStickyContainer', MkStickyContainer);
@ -53,7 +51,6 @@ declare module '@vue/runtime-core' {
MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading;
MkError: typeof MkError;
MkAd: typeof MkAd;
MkPageHeader: typeof MkPageHeader;
MkSpacer: typeof MkSpacer;
MkStickyContainer: typeof MkStickyContainer;

View file

@ -9,7 +9,7 @@
<template #default="{ items: notes }">
<div class="giivymft" :class="{ noGap }">
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" class="notes">
<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/>
</XList>
</div>

View file

@ -93,14 +93,6 @@ const init = async (): Promise<void> => {
...params,
limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 2) item._shouldInsertAd_ = true;
} else {
if (i === 3) item._shouldInsertAd_ = true;
}
}
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
res.pop();
items.value = props.pagination.reversed ? [...res].reverse() : res;
@ -137,14 +129,6 @@ const fetchMore = async (): Promise<void> => {
untilId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
}),
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 9) item._shouldInsertAd_ = true;
} else {
if (i === 10) item._shouldInsertAd_ = true;
}
}
if (res.length > SECOND_FETCH_LIMIT) {
res.pop();
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);

View file

@ -1,133 +0,0 @@
<template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="uqshojas">
<div v-for="ad in ads" class="_panel _formRoot ad">
<MkAd v-if="ad.url" :specify="ad"/>
<MkInput v-model="ad.url" type="url" class="_formBlock">
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl" class="_formBlock">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<FormRadios v-model="ad.place" class="_formBlock">
<template #label>Form</template>
<option value="square">square</option>
<option value="horizontal">horizontal</option>
<option value="horizontal-big">horizontal-big</option>
</FormRadios>
<!--
<div style="margin: 32px 0;">
{{ i18n.ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
<MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div>
-->
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="buttons _formBlock">
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let ads: any[] = $ref([]);
os.api('admin/ad/list').then(adsResponse => {
ads = adsResponse;
});
function add() {
ads.unshift({
id: null,
memo: '',
place: 'square',
priority: 'middle',
ratio: 1,
url: '',
imageUrl: null,
expiresAt: null,
});
}
function remove(ad) {
os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
ads = ads.filter(x => x !== ad);
os.apiWithDialog('admin/ad/delete', {
id: ad.id,
});
});
}
function save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime(),
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime(),
});
}
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
});
</script>
<style lang="scss" scoped>
.uqshojas {
> .ad {
padding: 32px;
&:not(:last-child) {
margin-bottom: var(--margin);
}
}
}
</style>

View file

@ -126,11 +126,6 @@ const menuDef = $computed(() => [{
text: i18n.ts.announcements,
to: '/admin/announcements',
active: props.initialPage === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.ts.ads,
to: '/admin/ads',
active: props.initialPage === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.ts.abuseReports,
@ -205,7 +200,6 @@ const component = $computed(() => {
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));

View file

@ -11,7 +11,7 @@
</template>
<template #default="{ items }">
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false">
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
</XList>
</template>

View file

@ -47,7 +47,6 @@
<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">

View file

@ -59,10 +59,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: [],
},
mutedAds: {
where: 'account',
default: [] as string[],
},
menu: {
where: 'deviceAccount',

View file

@ -1,7 +1,6 @@
<template>
<div class="ddiqwdnk">
<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value.filter(w => w.place === place)" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
<MkAd class="a" :prefer="['square']"/>
<button v-if="editMode" class="_textButton edit" style="font-size: 0.9em;" @click="editMode = false"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
<button v-else class="_textButton edit" style="font-size: 0.9em;" @click="editMode = true"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>