Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
syuilo | a55211744b | |
syuilo | 346282ba9d | |
syuilo | 7696792386 | |
syuilo | 66d6196df1 | |
syuilo | 4939a3d4a3 | |
syuilo | 3cb5d6d88e |
|
@ -747,6 +747,7 @@ gallery: "ギャラリー"
|
|||
recentPosts: "最近の投稿"
|
||||
popularPosts: "人気の投稿"
|
||||
shareWithNote: "ノートで共有"
|
||||
ads: "広告"
|
||||
|
||||
_gallery:
|
||||
my: "自分の投稿"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class ad1620019354680 implements MigrationInterface {
|
||||
name = 'ad1620019354680'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "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, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, CONSTRAINT "PK_0193d5ef09746e88e9ea92c634d" PRIMARY KEY ("id")); COMMENT ON COLUMN "ad"."createdAt" IS 'The created date of the Ad.'; COMMENT ON COLUMN "ad"."expiresAt" IS 'The expired date of the Ad.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_2da24ce20ad209f1d9dc032457" ON "ad" ("expiresAt") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_2da24ce20ad209f1d9dc032457"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_1129c2ef687fc272df040bafaa"`);
|
||||
await queryRunner.query(`DROP TABLE "ad"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -32,7 +32,6 @@
|
|||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"constantinople": "^4.0.1",
|
||||
"gulp/gulp-cli/yargs/yargs-parser": "5.0.0-security.0",
|
||||
"jsonld/rdf-canonize/node-forge": "0.10.0",
|
||||
"lodash": "^4.17.20"
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, h, TransitionGroup } from 'vue';
|
||||
import MkAd from '@client/components/global/ad.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
@ -58,11 +59,7 @@ export default defineComponent({
|
|||
|
||||
if (
|
||||
i != this.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
||||
!item._prId_ &&
|
||||
!this.items[i + 1]._prId_ &&
|
||||
!item._featuredId_ &&
|
||||
!this.items[i + 1]._featuredId_
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: 'separator',
|
||||
|
@ -86,7 +83,14 @@ export default defineComponent({
|
|||
|
||||
return [el, separator];
|
||||
} else {
|
||||
return el;
|
||||
if (i === 3) {
|
||||
return [h(MkAd, {
|
||||
class: 'ad',
|
||||
key: i + ':ad',
|
||||
}), el];
|
||||
} else {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div class="qiivuoyo" v-if="ad">
|
||||
<div class="main" :class="ad.place" v-if="!showMenu">
|
||||
<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 class="menu" v-else>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
prefer: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const showMenu = ref(false);
|
||||
const toggleMenu = () => {
|
||||
showMenu.value = !showMenu.value;
|
||||
};
|
||||
|
||||
let ads = this.$instance.ads.find(ad => ad.place === props.prefer);
|
||||
|
||||
if (ads.length === 0) {
|
||||
ads = this.$instance.ads.find(ad => ad.place === 'square');
|
||||
}
|
||||
|
||||
const ad = ads.length === 0 ? null : ads[Math.floor(Math.random() * ads.length)];
|
||||
|
||||
return {
|
||||
ad,
|
||||
showMenu,
|
||||
toggleMenu,
|
||||
};
|
||||
}
|
||||
});
|
||||
</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 {
|
||||
> a {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
|
||||
&.square {
|
||||
> a {
|
||||
max-width: min(300px, 100%);
|
||||
max-height: min(300px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
> a {
|
||||
max-width: 100%;
|
||||
max-height: min(100px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
> a {
|
||||
max-width: min(100px, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -12,8 +12,10 @@ import url from './global/url.vue';
|
|||
import i18n from './global/i18n';
|
||||
import loading from './global/loading.vue';
|
||||
import error from './global/error.vue';
|
||||
import ad from './global/ad.vue';
|
||||
|
||||
export default function(app: App) {
|
||||
app.component('I18n', i18n);
|
||||
app.component('Mfm', mfm);
|
||||
app.component('MkA', a);
|
||||
app.component('MkAcct', acct);
|
||||
|
@ -25,5 +27,5 @@ export default function(app: App) {
|
|||
app.component('MkUrl', url);
|
||||
app.component('MkLoading', loading);
|
||||
app.component('MkError', error);
|
||||
app.component('I18n', i18n);
|
||||
app.component('MkAd', ad);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div class="uqshojas">
|
||||
<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
||||
<section class="_card _gap announcements" v-for="announcement in announcements">
|
||||
<div class="_content announcement">
|
||||
<MkInput v-model:value="announcement.title">
|
||||
<span>{{ $ts.title }}</span>
|
||||
</MkInput>
|
||||
<MkTextarea v-model:value="announcement.text">
|
||||
<span>{{ $ts.text }}</span>
|
||||
</MkTextarea>
|
||||
<MkInput v-model:value="announcement.imageUrl">
|
||||
<span>{{ $ts.imageUrl }}</span>
|
||||
</MkInput>
|
||||
<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
|
||||
<div class="buttons">
|
||||
<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkInput from '@client/components/ui/input.vue';
|
||||
import MkTextarea from '@client/components/ui/textarea.vue';
|
||||
import * as os from '@client/os';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.ads,
|
||||
icon: 'fas fa-audio-description'
|
||||
},
|
||||
ads: [],
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
os.api('admin/ad/list').then(ads => {
|
||||
this.ads = ads;
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uqshojas {
|
||||
margin: var(--margin);
|
||||
}
|
||||
</style>
|
|
@ -23,6 +23,7 @@
|
|||
<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
|
||||
<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
|
||||
<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
|
||||
<FormLink :active="page === 'ads'" replace to="/instance/ads"><template #icon><i class="fas fa-audio-description"></i></template>{{ $ts.ads }}</FormLink>
|
||||
<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
|
@ -102,6 +103,7 @@ export default defineComponent({
|
|||
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'));
|
||||
|
|
|
@ -42,11 +42,7 @@ export default defineComponent({
|
|||
|
||||
if (
|
||||
i != this.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
||||
!item._prId_ &&
|
||||
!this.items[i + 1]._prId_ &&
|
||||
!item._featuredId_ &&
|
||||
!this.items[i + 1]._featuredId_
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: 'separator',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="efzpzdvf">
|
||||
<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
|
||||
<MkAd/>
|
||||
|
||||
<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
|
||||
<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
|
||||
|
|
|
@ -70,6 +70,7 @@ import { Channel } from '../models/entities/channel';
|
|||
import { ChannelFollowing } from '../models/entities/channel-following';
|
||||
import { ChannelNotePining } from '../models/entities/channel-note-pining';
|
||||
import { RegistryItem } from '../models/entities/registry-item';
|
||||
import { Ad } from '../models/entities/ad';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
|
@ -169,6 +170,7 @@ export const entities = [
|
|||
ChannelFollowing,
|
||||
ChannelNotePining,
|
||||
RegistryItem,
|
||||
Ad,
|
||||
...charts as any
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
|
||||
import { id } from '../id';
|
||||
|
||||
@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: 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ import { MutedNote } from './entities/muted-note';
|
|||
import { ChannelFollowing } from './entities/channel-following';
|
||||
import { ChannelNotePining } from './entities/channel-note-pining';
|
||||
import { RegistryItem } from './entities/registry-item';
|
||||
import { Ad } from './entities/ad';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
|
@ -122,3 +123,4 @@ export const Channels = getCustomRepository(ChannelRepository);
|
|||
export const ChannelFollowings = getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
||||
export const RegistryItems = getRepository(RegistryItem);
|
||||
export const Ads = getRepository(Ad);
|
||||
|
|
|
@ -200,8 +200,6 @@ export class NoteRepository extends Repository<Note> {
|
|||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri || undefined,
|
||||
url: note.url || undefined,
|
||||
_featuredId_: (note as any)._featuredId_ || undefined,
|
||||
_prId_: (note as any)._prId_ || undefined,
|
||||
|
||||
...(opts.detail ? {
|
||||
reply: note.replyId ? this.pack(note.reply || note.replyId, me, {
|
||||
|
@ -448,14 +446,7 @@ export const packedNoteSchema = {
|
|||
optional: false as const, nullable: true as const,
|
||||
description: 'The human readable url of a note. it will be null when the note is local.',
|
||||
},
|
||||
_featuredId_: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
},
|
||||
_prId_: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
},
|
||||
|
||||
myReaction: {
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Ads } from '../../../../../models';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true as const,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
url: {
|
||||
validator: $.str.min(1)
|
||||
},
|
||||
memo: {
|
||||
validator: $.str
|
||||
},
|
||||
place: {
|
||||
validator: $.str
|
||||
},
|
||||
expiresAt: {
|
||||
validator: $.num.int()
|
||||
},
|
||||
imageUrl: {
|
||||
validator: $.str.min(1)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
await Ads.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
url: ps.url,
|
||||
imageUrl: ps.imageUrl,
|
||||
place: ps.place,
|
||||
memo: ps.memo,
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { Ads } from '../../../../../models';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true as const,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
||||
|
||||
const ads = await query.take(ps.limit!).getMany();
|
||||
|
||||
return ads;
|
||||
});
|
|
@ -2,8 +2,9 @@ import $ from 'cafy';
|
|||
import config from '@/config';
|
||||
import define from '../define';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { Emojis, Users } from '../../../models';
|
||||
import { Ads, Emojis, Users } from '../../../models';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
|
||||
import { MoreThan } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -193,6 +194,30 @@ export const meta = {
|
|||
}
|
||||
}
|
||||
},
|
||||
ads: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
place: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
url: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'url'
|
||||
},
|
||||
imageUrl: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'url'
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
requireSetup: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
|
@ -443,6 +468,12 @@ export default define(meta, async (ps, me) => {
|
|||
}
|
||||
});
|
||||
|
||||
const ads = await Ads.find({
|
||||
where: {
|
||||
expiresAt: MoreThan(new Date())
|
||||
},
|
||||
});
|
||||
|
||||
const response: any = {
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
|
@ -477,6 +508,11 @@ export default define(meta, async (ps, me) => {
|
|||
logoImageUrl: instance.logoImageUrl,
|
||||
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
|
||||
emojis: await Emojis.packMany(emojis),
|
||||
ads: ads.map(ad => ({
|
||||
url: ad.url,
|
||||
place: ad.place,
|
||||
imageUrl: ad.imageUrl,
|
||||
})),
|
||||
enableEmail: instance.enableEmail,
|
||||
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
|
|
Loading…
Reference in New Issue