mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	add: profile backgrounds
This commit is contained in:
		
							parent
							
								
									6dd0b88050
								
							
						
					
					
						commit
						4e64397635
					
				
					 14 changed files with 205 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 });
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (user.backgroundId) {
 | 
			
		||||
			q.andWhere('file.id != :backgroundId', { backgroundId: user.backgroundId });
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//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.orderBy('file.id', 'ASC');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -454,9 +454,10 @@ export class ApRendererService {
 | 
			
		|||
		const id = this.userEntityService.genLocalUserUri(user.id);
 | 
			
		||||
		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.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
 | 
			
		||||
			user.backgroundId ? this.driveFilesRepository.findOneBy({ id: user.backgroundId }) : undefined,
 | 
			
		||||
			this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -496,6 +497,7 @@ export class ApRendererService {
 | 
			
		|||
			summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
 | 
			
		||||
			icon: avatar ? this.renderImage(avatar) : null,
 | 
			
		||||
			image: banner ? this.renderImage(banner) : null,
 | 
			
		||||
			backgroundUrl: background ? this.renderImage(background) : null,
 | 
			
		||||
			tag,
 | 
			
		||||
			manuallyApprovesFollowers: user.isLocked,
 | 
			
		||||
			discoverable: user.isExplorable,
 | 
			
		||||
| 
						 | 
				
			
			@ -650,6 +652,9 @@ export class ApRendererService {
 | 
			
		|||
					// Firefish
 | 
			
		||||
					firefish: "https://joinfirefish.org/ns#",
 | 
			
		||||
					speakAsCat: "firefish:speakAsCat",
 | 
			
		||||
					// Sharkey
 | 
			
		||||
					sharkey: "https://joinsharkey.org/ns#",
 | 
			
		||||
					backgroundUrl: "sharkey:backgroundUrl",
 | 
			
		||||
					// vcard
 | 
			
		||||
					vcard: 'http://www.w3.org/2006/vcard/ns#',
 | 
			
		||||
				},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -225,8 +225,8 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		return null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
 | 
			
		||||
		const [avatar, banner] = await Promise.all([icon, image].map(img => {
 | 
			
		||||
	private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>> {
 | 
			
		||||
		const [avatar, banner, background] = await Promise.all([icon, image].map(img => {
 | 
			
		||||
			if (img == null) return null;
 | 
			
		||||
			if (user == null) throw new Error('failed to create user: user is null');
 | 
			
		||||
			return this.apImageService.resolveImage(user, img).catch(() => null);
 | 
			
		||||
| 
						 | 
				
			
			@ -235,10 +235,13 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		return {
 | 
			
		||||
			avatarId: avatar?.id ?? null,
 | 
			
		||||
			bannerId: banner?.id ?? null,
 | 
			
		||||
			backgroundId: background?.id ?? null,
 | 
			
		||||
			avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
 | 
			
		||||
			bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
 | 
			
		||||
			backgroundUrl: background ? this.driveFileEntityService.getPublicUrl(background) : null,
 | 
			
		||||
			avatarBlurhash: avatar?.blurhash ?? null,
 | 
			
		||||
			bannerBlurhash: banner?.blurhash ?? null,
 | 
			
		||||
			backgroundBlurhash: background?.blurhash ?? null
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -308,6 +308,14 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
				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 isMe = meId === user.id;
 | 
			
		||||
| 
						 | 
				
			
			@ -385,6 +393,8 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
				lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
 | 
			
		||||
				bannerUrl: user.bannerUrl,
 | 
			
		||||
				bannerBlurhash: user.bannerBlurhash,
 | 
			
		||||
				backgroundUrl: user.backgroundUrl,
 | 
			
		||||
				backgroundBlurhash: user.backgroundBlurhash,
 | 
			
		||||
				isLocked: user.isLocked,
 | 
			
		||||
				isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
 | 
			
		||||
				isSuspended: user.isSuspended ?? falsy,
 | 
			
		||||
| 
						 | 
				
			
			@ -429,6 +439,7 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
			...(opts.detail && isMe ? {
 | 
			
		||||
				avatarId: user.avatarId,
 | 
			
		||||
				bannerId: user.bannerId,
 | 
			
		||||
				backgroundId: user.backgroundId,
 | 
			
		||||
				isModerator: isModerator,
 | 
			
		||||
				isAdmin: isAdmin,
 | 
			
		||||
				injectFeaturedNote: profile!.injectFeaturedNote,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,6 +124,19 @@ export class MiUser {
 | 
			
		|||
	@JoinColumn()
 | 
			
		||||
	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', {
 | 
			
		||||
		length: 512, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			@ -134,6 +147,11 @@ export class MiUser {
 | 
			
		|||
	})
 | 
			
		||||
	public bannerUrl: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 512, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public backgroundUrl: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			@ -144,6 +162,11 @@ export class MiUser {
 | 
			
		|||
	})
 | 
			
		||||
	public bannerBlurhash: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public backgroundBlurhash: string | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128, array: true, default: '{}',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,6 +122,15 @@ export const packedUserDetailedNotMeOnlySchema = {
 | 
			
		|||
			type: 'string',
 | 
			
		||||
			nullable: true, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		backgroundUrl: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			format: 'url',
 | 
			
		||||
			nullable: true, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		backgroundBlurhash: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			nullable: true, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		isLocked: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -304,6 +313,11 @@ export const packedMeDetailedOnlySchema = {
 | 
			
		|||
			nullable: true, optional: false,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
		},
 | 
			
		||||
		backgroundId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			nullable: true, optional: false,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
		},
 | 
			
		||||
		injectFeaturedNote: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			nullable: true, optional: false,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,6 +60,12 @@ export const meta = {
 | 
			
		|||
			id: '0d8f5629-f210-41c2-9433-735831a58595',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchBackground: {
 | 
			
		||||
			message: 'No such background file.',
 | 
			
		||||
			code: 'NO_SUCH_BACKGROUND',
 | 
			
		||||
			id: '0d8f5629-f210-41c2-9433-735831a58582',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		avatarNotAnImage: {
 | 
			
		||||
			message: 'The file specified as an avatar is not an image.',
 | 
			
		||||
			code: 'AVATAR_NOT_AN_IMAGE',
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +78,12 @@ export const meta = {
 | 
			
		|||
			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: {
 | 
			
		||||
			message: '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 },
 | 
			
		||||
		avatarId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
		bannerId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
		backgroundId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
		fields: {
 | 
			
		||||
			type: 'array',
 | 
			
		||||
			minItems: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -300,6 +313,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				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) {
 | 
			
		||||
				const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,8 @@ describe('ユーザー', () => {
 | 
			
		|||
			lastFetchedAt: user.lastFetchedAt,
 | 
			
		||||
			bannerUrl: user.bannerUrl,
 | 
			
		||||
			bannerBlurhash: user.bannerBlurhash,
 | 
			
		||||
			backgroundUrl: user.backgroundUrl,
 | 
			
		||||
			backgroundBlurhash: user.backgroundBlurhash,
 | 
			
		||||
			isLocked: user.isLocked,
 | 
			
		||||
			isSilenced: user.isSilenced,
 | 
			
		||||
			isSuspended: user.isSuspended,
 | 
			
		||||
| 
						 | 
				
			
			@ -366,6 +368,8 @@ describe('ユーザー', () => {
 | 
			
		|||
		assert.strictEqual(response.lastFetchedAt, null);
 | 
			
		||||
		assert.strictEqual(response.bannerUrl, null);
 | 
			
		||||
		assert.strictEqual(response.bannerBlurhash, null);
 | 
			
		||||
		assert.strictEqual(response.backgroundUrl, null);
 | 
			
		||||
		assert.strictEqual(response.backgroundBlurhash, null);
 | 
			
		||||
		assert.strictEqual(response.isLocked, false);
 | 
			
		||||
		assert.strictEqual(response.isSilenced, false);
 | 
			
		||||
		assert.strictEqual(response.isSuspended, false);
 | 
			
		||||
| 
						 | 
				
			
			@ -561,6 +565,31 @@ describe('ユーザー', () => {
 | 
			
		|||
		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
 | 
			
		||||
	//#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"/>
 | 
			
		||||
			<p :class="$style.labelText">{{ i18n.ts.banner }}</p>
 | 
			
		||||
		</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]">
 | 
			
		||||
			<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
 | 
			
		||||
			<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
 | 
			
		||||
			<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
 | 
			
		||||
		</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>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	<MkInput v-model="profile.name" :max="30" manualSave>
 | 
			
		||||
		<template #label>{{ i18n.ts._profile.name }}</template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
| 
						 | 
				
			
			@ -254,6 +256,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 headerTabs = $computed(() => []);
 | 
			
		||||
| 
						 | 
				
			
			@ -292,6 +319,11 @@ definePageMetadata({
 | 
			
		|||
	top: 16px;
 | 
			
		||||
	right: 16px;
 | 
			
		||||
}
 | 
			
		||||
.backgroundEdit {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 103px;
 | 
			
		||||
	right: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.metadataRoot {
 | 
			
		||||
	container-type: inline-size;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
-->
 | 
			
		||||
 | 
			
		||||
<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 class="main _gaps">
 | 
			
		||||
			<!-- 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 () => {
 | 
			
		||||
	await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -338,6 +345,24 @@ onUnmounted(() => {
 | 
			
		|||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.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 {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -414,6 +414,7 @@ export type Endpoints = {
 | 
			
		|||
		birthday?: string | null;
 | 
			
		||||
		avatarId?: DriveFile['id'] | null;
 | 
			
		||||
		bannerId?: DriveFile['id'] | null;
 | 
			
		||||
		backgroundId?: DriveFile['id'] | null;
 | 
			
		||||
		fields?: {
 | 
			
		||||
			name: string;
 | 
			
		||||
			value: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,8 @@ export type UserDetailed = UserLite & {
 | 
			
		|||
	bannerBlurhash: string | null;
 | 
			
		||||
	bannerColor: string | null;
 | 
			
		||||
	bannerUrl: string | null;
 | 
			
		||||
	backgroundUrl: string | null;
 | 
			
		||||
	backgroundBlurhash: string | null;
 | 
			
		||||
	birthday: string | null;
 | 
			
		||||
	createdAt: DateString;
 | 
			
		||||
	description: string | null;
 | 
			
		||||
| 
						 | 
				
			
			@ -88,6 +90,7 @@ export type UserList = {
 | 
			
		|||
export type MeDetailed = UserDetailed & {
 | 
			
		||||
	avatarId: DriveFile['id'];
 | 
			
		||||
	bannerId: DriveFile['id'];
 | 
			
		||||
	backgroundId: DriveFile['id'];
 | 
			
		||||
	autoAcceptFollowed: boolean;
 | 
			
		||||
	alwaysMarkNsfw: boolean;
 | 
			
		||||
	carefulBot: boolean;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue