merge: profile backgrounds (#56)
This commit is contained in:
commit
a5dd169475
14 changed files with 204 additions and 4 deletions
19
packages/backend/migration/1696548899000-background.js
Normal file
19
packages/backend/migration/1696548899000-background.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export class Background1696548899000 {
|
||||||
|
name = 'Background1696548899000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundId" character varying(32)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5" UNIQUE ("backgroundId")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n" FOREIGN KEY ("backgroundId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundUrl" character varying(512)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundBlurhash" character varying(128)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundUrl"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundBlurhash"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -423,6 +423,10 @@ export class DriveService {
|
||||||
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
|
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.backgroundId) {
|
||||||
|
q.andWhere('file.id != :backgroundId', { backgroundId: user.backgroundId });
|
||||||
|
}
|
||||||
|
|
||||||
//This selete is hard coded, be careful if change database schema
|
//This selete is hard coded, be careful if change database schema
|
||||||
q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
|
q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
|
||||||
q.orderBy('file.id', 'ASC');
|
q.orderBy('file.id', 'ASC');
|
||||||
|
|
|
@ -454,9 +454,10 @@ export class ApRendererService {
|
||||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||||
const isSystem = user.username.includes('.');
|
const isSystem = user.username.includes('.');
|
||||||
|
|
||||||
const [avatar, banner, profile] = await Promise.all([
|
const [avatar, banner, background, profile] = await Promise.all([
|
||||||
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
|
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
|
||||||
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
|
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
|
||||||
|
user.backgroundId ? this.driveFilesRepository.findOneBy({ id: user.backgroundId }) : undefined,
|
||||||
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -496,6 +497,7 @@ export class ApRendererService {
|
||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
icon: avatar ? this.renderImage(avatar) : null,
|
icon: avatar ? this.renderImage(avatar) : null,
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
|
backgroundUrl: background ? this.renderImage(background) : null,
|
||||||
tag,
|
tag,
|
||||||
manuallyApprovesFollowers: user.isLocked,
|
manuallyApprovesFollowers: user.isLocked,
|
||||||
discoverable: user.isExplorable,
|
discoverable: user.isExplorable,
|
||||||
|
@ -650,6 +652,9 @@ export class ApRendererService {
|
||||||
// Firefish
|
// Firefish
|
||||||
firefish: "https://joinfirefish.org/ns#",
|
firefish: "https://joinfirefish.org/ns#",
|
||||||
speakAsCat: "firefish:speakAsCat",
|
speakAsCat: "firefish:speakAsCat",
|
||||||
|
// Sharkey
|
||||||
|
sharkey: "https://joinsharkey.org/ns#",
|
||||||
|
backgroundUrl: "sharkey:backgroundUrl",
|
||||||
// vcard
|
// vcard
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
},
|
},
|
||||||
|
|
|
@ -225,8 +225,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
|
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>> {
|
||||||
const [avatar, banner] = await Promise.all([icon, image].map(img => {
|
const [avatar, banner, background] = await Promise.all([icon, image].map(img => {
|
||||||
if (img == null) return null;
|
if (img == null) return null;
|
||||||
if (user == null) throw new Error('failed to create user: user is null');
|
if (user == null) throw new Error('failed to create user: user is null');
|
||||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||||
|
@ -235,10 +235,13 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return {
|
return {
|
||||||
avatarId: avatar?.id ?? null,
|
avatarId: avatar?.id ?? null,
|
||||||
bannerId: banner?.id ?? null,
|
bannerId: banner?.id ?? null,
|
||||||
|
backgroundId: background?.id ?? null,
|
||||||
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
||||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||||
|
backgroundUrl: background ? this.driveFileEntityService.getPublicUrl(background) : null,
|
||||||
avatarBlurhash: avatar?.blurhash ?? null,
|
avatarBlurhash: avatar?.blurhash ?? null,
|
||||||
bannerBlurhash: banner?.blurhash ?? null,
|
bannerBlurhash: banner?.blurhash ?? null,
|
||||||
|
backgroundBlurhash: background?.blurhash ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -308,6 +308,14 @@ export class UserEntityService implements OnModuleInit {
|
||||||
bannerBlurhash: banner.blurhash,
|
bannerBlurhash: banner.blurhash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (user.backgroundId != null && user.backgroundUrl === null) {
|
||||||
|
const background = await this.driveFilesRepository.findOneByOrFail({ id: user.backgroundId });
|
||||||
|
user.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
backgroundUrl: user.backgroundUrl,
|
||||||
|
backgroundBlurhash: background.blurhash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const isMe = meId === user.id;
|
const isMe = meId === user.id;
|
||||||
|
@ -385,6 +393,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||||
bannerUrl: user.bannerUrl,
|
bannerUrl: user.bannerUrl,
|
||||||
bannerBlurhash: user.bannerBlurhash,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
backgroundUrl: user.backgroundUrl,
|
||||||
|
backgroundBlurhash: user.backgroundBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||||
isSuspended: user.isSuspended ?? falsy,
|
isSuspended: user.isSuspended ?? falsy,
|
||||||
|
@ -429,6 +439,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
...(opts.detail && isMe ? {
|
...(opts.detail && isMe ? {
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
backgroundId: user.backgroundId,
|
||||||
isModerator: isModerator,
|
isModerator: isModerator,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
injectFeaturedNote: profile!.injectFeaturedNote,
|
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||||
|
|
|
@ -124,6 +124,19 @@ export class MiUser {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public banner: MiDriveFile | null;
|
public banner: MiDriveFile | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
comment: 'The ID of background DriveFile.',
|
||||||
|
})
|
||||||
|
public backgroundId: MiDriveFile['id'] | null;
|
||||||
|
|
||||||
|
@OneToOne(type => MiDriveFile, {
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public background: MiDriveFile | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512, nullable: true,
|
length: 512, nullable: true,
|
||||||
})
|
})
|
||||||
|
@ -134,6 +147,11 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public bannerUrl: string | null;
|
public bannerUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 512, nullable: true,
|
||||||
|
})
|
||||||
|
public backgroundUrl: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 128, nullable: true,
|
length: 128, nullable: true,
|
||||||
})
|
})
|
||||||
|
@ -144,6 +162,11 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public bannerBlurhash: string | null;
|
public bannerBlurhash: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128, nullable: true,
|
||||||
|
})
|
||||||
|
public backgroundBlurhash: string | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 128, array: true, default: '{}',
|
length: 128, array: true, default: '{}',
|
||||||
|
|
|
@ -122,6 +122,15 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
|
backgroundUrl: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'url',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
|
backgroundBlurhash: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
isLocked: {
|
isLocked: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@ -304,6 +313,11 @@ export const packedMeDetailedOnlySchema = {
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
backgroundId: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
injectFeaturedNote: {
|
injectFeaturedNote: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -60,6 +60,12 @@ export const meta = {
|
||||||
id: '0d8f5629-f210-41c2-9433-735831a58595',
|
id: '0d8f5629-f210-41c2-9433-735831a58595',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noSuchBackground: {
|
||||||
|
message: 'No such background file.',
|
||||||
|
code: 'NO_SUCH_BACKGROUND',
|
||||||
|
id: '0d8f5629-f210-41c2-9433-735831a58582',
|
||||||
|
},
|
||||||
|
|
||||||
avatarNotAnImage: {
|
avatarNotAnImage: {
|
||||||
message: 'The file specified as an avatar is not an image.',
|
message: 'The file specified as an avatar is not an image.',
|
||||||
code: 'AVATAR_NOT_AN_IMAGE',
|
code: 'AVATAR_NOT_AN_IMAGE',
|
||||||
|
@ -72,6 +78,12 @@ export const meta = {
|
||||||
id: '75aedb19-2afd-4e6d-87fc-67941256fa60',
|
id: '75aedb19-2afd-4e6d-87fc-67941256fa60',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
backgroundNotAnImage: {
|
||||||
|
message: 'The file specified as a background is not an image.',
|
||||||
|
code: 'BACKGROUND_NOT_AN_IMAGE',
|
||||||
|
id: '75aedb19-2afd-4e6d-87fc-67941256fa40',
|
||||||
|
},
|
||||||
|
|
||||||
noSuchPage: {
|
noSuchPage: {
|
||||||
message: 'No such page.',
|
message: 'No such page.',
|
||||||
code: 'NO_SUCH_PAGE',
|
code: 'NO_SUCH_PAGE',
|
||||||
|
@ -133,6 +145,7 @@ export const paramDef = {
|
||||||
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
||||||
avatarId: { type: 'string', format: 'misskey:id', nullable: true },
|
avatarId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
|
backgroundId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
fields: {
|
fields: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
minItems: 0,
|
minItems: 0,
|
||||||
|
@ -300,6 +313,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
updates.bannerBlurhash = null;
|
updates.bannerBlurhash = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.backgroundId) {
|
||||||
|
const background = await this.driveFilesRepository.findOneBy({ id: ps.backgroundId });
|
||||||
|
|
||||||
|
if (background == null || background.userId !== user.id) throw new ApiError(meta.errors.noSuchBackground);
|
||||||
|
if (!background.type.startsWith('image/')) throw new ApiError(meta.errors.backgroundNotAnImage);
|
||||||
|
|
||||||
|
updates.backgroundId = background.id;
|
||||||
|
updates.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
|
||||||
|
updates.backgroundBlurhash = background.blurhash;
|
||||||
|
} else if (ps.backgroundId === null) {
|
||||||
|
updates.backgroundId = null;
|
||||||
|
updates.backgroundUrl = null;
|
||||||
|
updates.backgroundBlurhash = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.pinnedPageId) {
|
if (ps.pinnedPageId) {
|
||||||
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
|
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
|
||||||
|
|
||||||
|
|
|
@ -95,6 +95,8 @@ describe('ユーザー', () => {
|
||||||
lastFetchedAt: user.lastFetchedAt,
|
lastFetchedAt: user.lastFetchedAt,
|
||||||
bannerUrl: user.bannerUrl,
|
bannerUrl: user.bannerUrl,
|
||||||
bannerBlurhash: user.bannerBlurhash,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
backgroundUrl: user.backgroundUrl,
|
||||||
|
backgroundBlurhash: user.backgroundBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: user.isSilenced,
|
isSilenced: user.isSilenced,
|
||||||
isSuspended: user.isSuspended,
|
isSuspended: user.isSuspended,
|
||||||
|
@ -366,6 +368,8 @@ describe('ユーザー', () => {
|
||||||
assert.strictEqual(response.lastFetchedAt, null);
|
assert.strictEqual(response.lastFetchedAt, null);
|
||||||
assert.strictEqual(response.bannerUrl, null);
|
assert.strictEqual(response.bannerUrl, null);
|
||||||
assert.strictEqual(response.bannerBlurhash, null);
|
assert.strictEqual(response.bannerBlurhash, null);
|
||||||
|
assert.strictEqual(response.backgroundUrl, null);
|
||||||
|
assert.strictEqual(response.backgroundBlurhash, null);
|
||||||
assert.strictEqual(response.isLocked, false);
|
assert.strictEqual(response.isLocked, false);
|
||||||
assert.strictEqual(response.isSilenced, false);
|
assert.strictEqual(response.isSilenced, false);
|
||||||
assert.strictEqual(response.isSuspended, false);
|
assert.strictEqual(response.isSuspended, false);
|
||||||
|
@ -561,6 +565,31 @@ describe('ユーザー', () => {
|
||||||
assert.deepStrictEqual(response2, expected2, inspect(parameters));
|
assert.deepStrictEqual(response2, expected2, inspect(parameters));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('を書き換えることができる(Background)', async () => {
|
||||||
|
const aliceFile = (await uploadFile(alice)).body;
|
||||||
|
const parameters = { bannerId: aliceFile.id };
|
||||||
|
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
|
||||||
|
assert.match(response.backgroundUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
|
||||||
|
assert.match(response.backgroundBlurhash ?? '.', /[ -~]{54}/);
|
||||||
|
const expected = {
|
||||||
|
...meDetailed(alice, true),
|
||||||
|
backgroundId: aliceFile.id,
|
||||||
|
backgroundBlurhash: response.baackgroundBlurhash,
|
||||||
|
backgroundUrl: response.backgroundUrl,
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(response, expected, inspect(parameters));
|
||||||
|
|
||||||
|
const parameters2 = { backgroundId: null };
|
||||||
|
const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice });
|
||||||
|
const expected2 = {
|
||||||
|
...meDetailed(alice, true),
|
||||||
|
backgroundId: null,
|
||||||
|
backgroundBlurhash: null,
|
||||||
|
backgroundUrl: null,
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(response2, expected2, inspect(parameters));
|
||||||
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region 自分の情報の更新(i/pin, i/unpin)
|
//#region 自分の情報の更新(i/pin, i/unpin)
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
||||||
<p :class="$style.labelText">{{ i18n.ts.banner }}</p>
|
<p :class="$style.labelText">{{ i18n.ts.banner }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="$i?.backgroundId == file.id" :class="[$style.label]">
|
||||||
|
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
||||||
|
<p :class="$style.labelText">Background</p>
|
||||||
|
</div>
|
||||||
<div v-if="file.isSensitive" :class="[$style.label, $style.red]">
|
<div v-if="file.isSensitive" :class="[$style.label, $style.red]">
|
||||||
<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
|
<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
|
||||||
<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
|
<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
|
||||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
|
<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
|
||||||
<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
|
<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
<MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">Change Background</MkButton>
|
||||||
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
|
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -254,6 +255,31 @@ function changeBanner(ev) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeBackground(ev) {
|
||||||
|
selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => {
|
||||||
|
let originalOrCropped = file;
|
||||||
|
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
text: i18n.t('cropImageAsk'),
|
||||||
|
okText: i18n.ts.cropYes,
|
||||||
|
cancelText: i18n.ts.cropNo,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!canceled) {
|
||||||
|
originalOrCropped = await os.cropImage(file, {
|
||||||
|
aspectRatio: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const i = await os.apiWithDialog('i/update', {
|
||||||
|
backgroundId: originalOrCropped.id,
|
||||||
|
});
|
||||||
|
$i.backgroundId = i.backgroundId;
|
||||||
|
$i.backgroundUrl = i.backgroundUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
@ -292,6 +318,11 @@ definePageMetadata({
|
||||||
top: 16px;
|
top: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
|
.backgroundEdit {
|
||||||
|
position: absolute;
|
||||||
|
top: 103px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.metadataRoot {
|
.metadataRoot {
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkSpacer :contentMax="narrow ? 800 : 1100">
|
<MkSpacer :contentMax="narrow ? 800 : 1100" :style="background">
|
||||||
<div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
|
<div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
|
||||||
<div class="main _gaps">
|
<div class="main _gaps">
|
||||||
<!-- TODO -->
|
<!-- TODO -->
|
||||||
|
@ -236,6 +236,13 @@ if (props.user.listenbrainz) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const background = computed(() => {
|
||||||
|
if (props.user.backgroundUrl == null) return {};
|
||||||
|
return {
|
||||||
|
'--backgroundImageStatic': `url('${props.user.backgroundUrl}')`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
watch($$(moderationNote), async () => {
|
watch($$(moderationNote), async () => {
|
||||||
await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote });
|
await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote });
|
||||||
});
|
});
|
||||||
|
@ -338,6 +345,24 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.ftskorzw {
|
.ftskorzw {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--backgroundImageStatic);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(8px) opacity(0.6);
|
||||||
|
// Funny CSS schenanigans to make background escape container
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-left: -20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
margin-right: -20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-top: -20px;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
> .main {
|
> .main {
|
||||||
|
|
||||||
|
|
|
@ -414,6 +414,7 @@ export type Endpoints = {
|
||||||
birthday?: string | null;
|
birthday?: string | null;
|
||||||
avatarId?: DriveFile['id'] | null;
|
avatarId?: DriveFile['id'] | null;
|
||||||
bannerId?: DriveFile['id'] | null;
|
bannerId?: DriveFile['id'] | null;
|
||||||
|
backgroundId?: DriveFile['id'] | null;
|
||||||
fields?: {
|
fields?: {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
|
|
@ -35,6 +35,8 @@ export type UserDetailed = UserLite & {
|
||||||
bannerBlurhash: string | null;
|
bannerBlurhash: string | null;
|
||||||
bannerColor: string | null;
|
bannerColor: string | null;
|
||||||
bannerUrl: string | null;
|
bannerUrl: string | null;
|
||||||
|
backgroundUrl: string | null;
|
||||||
|
backgroundBlurhash: string | null;
|
||||||
birthday: string | null;
|
birthday: string | null;
|
||||||
createdAt: DateString;
|
createdAt: DateString;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
@ -88,6 +90,7 @@ export type UserList = {
|
||||||
export type MeDetailed = UserDetailed & {
|
export type MeDetailed = UserDetailed & {
|
||||||
avatarId: DriveFile['id'];
|
avatarId: DriveFile['id'];
|
||||||
bannerId: DriveFile['id'];
|
bannerId: DriveFile['id'];
|
||||||
|
backgroundId: DriveFile['id'];
|
||||||
autoAcceptFollowed: boolean;
|
autoAcceptFollowed: boolean;
|
||||||
alwaysMarkNsfw: boolean;
|
alwaysMarkNsfw: boolean;
|
||||||
carefulBot: boolean;
|
carefulBot: boolean;
|
||||||
|
|
Loading…
Reference in a new issue