add listenbrainz from sharkey
Some checks failed
Lint / pnpm_install (pull_request) Successful in 3m15s
Test (backend) / e2e (22.x) (pull_request) Failing after 9m0s
Test (backend) / unit (22.x) (pull_request) Successful in 9m59s
Test (frontend) / vitest (22.x) (pull_request) Failing after 4m43s
Test (production install and build) / production (22.x) (pull_request) Successful in 5m56s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 3m2s
Lint / lint (backend) (pull_request) Failing after 4m2s
Lint / lint (frontend) (pull_request) Failing after 8m30s
Lint / lint (misskey-js) (pull_request) Successful in 5m30s
Lint / lint (sw) (pull_request) Successful in 3m26s
Lint / typecheck (backend) (pull_request) Successful in 6m5s
Lint / typecheck (misskey-js) (pull_request) Successful in 11m58s
Some checks failed
Lint / pnpm_install (pull_request) Successful in 3m15s
Test (backend) / e2e (22.x) (pull_request) Failing after 9m0s
Test (backend) / unit (22.x) (pull_request) Successful in 9m59s
Test (frontend) / vitest (22.x) (pull_request) Failing after 4m43s
Test (production install and build) / production (22.x) (pull_request) Successful in 5m56s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 3m2s
Lint / lint (backend) (pull_request) Failing after 4m2s
Lint / lint (frontend) (pull_request) Failing after 8m30s
Lint / lint (misskey-js) (pull_request) Successful in 5m30s
Lint / lint (sw) (pull_request) Successful in 3m26s
Lint / typecheck (backend) (pull_request) Successful in 6m5s
Lint / typecheck (misskey-js) (pull_request) Successful in 11m58s
This commit is contained in:
parent
9dbb4124da
commit
85818f1610
12 changed files with 218 additions and 2 deletions
|
@ -305,6 +305,7 @@ activity: "Activity"
|
||||||
images: "Images"
|
images: "Images"
|
||||||
image: "Image"
|
image: "Image"
|
||||||
birthday: "Birthday"
|
birthday: "Birthday"
|
||||||
|
listenbrainzDescription: "Input your listenbrainz username here"
|
||||||
yearsOld: "{age} years old"
|
yearsOld: "{age} years old"
|
||||||
registeredDate: "Joined on"
|
registeredDate: "Joined on"
|
||||||
location: "Location"
|
location: "Location"
|
||||||
|
|
6
locales/index.d.ts
vendored
6
locales/index.d.ts
vendored
|
@ -1244,6 +1244,10 @@ export interface Locale extends ILocale {
|
||||||
* 誕生日
|
* 誕生日
|
||||||
*/
|
*/
|
||||||
"birthday": string;
|
"birthday": string;
|
||||||
|
/**
|
||||||
|
* listenbrainzのユーザー名を入力してください。
|
||||||
|
*/
|
||||||
|
"listenbrainzDescription": string;
|
||||||
/**
|
/**
|
||||||
* {age}歳
|
* {age}歳
|
||||||
*/
|
*/
|
||||||
|
@ -6843,7 +6847,7 @@ export interface Locale extends ILocale {
|
||||||
};
|
};
|
||||||
"_tutorialCompleted": {
|
"_tutorialCompleted": {
|
||||||
/**
|
/**
|
||||||
* Misskey初心者講座 修了証
|
* Forkey初心者講座 修了証
|
||||||
*/
|
*/
|
||||||
"title": string;
|
"title": string;
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -307,6 +307,7 @@ activity: "アクティビティ"
|
||||||
images: "画像"
|
images: "画像"
|
||||||
image: "画像"
|
image: "画像"
|
||||||
birthday: "誕生日"
|
birthday: "誕生日"
|
||||||
|
listenbrainzDescription: "listenbrainzのユーザー名を入力してください。"
|
||||||
yearsOld: "{age}歳"
|
yearsOld: "{age}歳"
|
||||||
registeredDate: "登録日"
|
registeredDate: "登録日"
|
||||||
location: "場所"
|
location: "場所"
|
||||||
|
|
20
packages/backend/migration/1691264431000-add-lb-to-user.js
Normal file
20
packages/backend/migration/1691264431000-add-lb-to-user.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export class AddLbToUser1691264431000 {
|
||||||
|
name = "AddLbToUser1691264431000";
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "user_profile"
|
||||||
|
ADD "listenbrainz" character varying(128) NULL
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
COMMENT ON COLUMN "user_profile"."listenbrainz"
|
||||||
|
IS 'listenbrainz username to fetch currently playing.'
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "user_profile" DROP COLUMN "listenbrainz"
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -530,6 +530,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
description: profile!.description,
|
description: profile!.description,
|
||||||
location: profile!.location,
|
location: profile!.location,
|
||||||
birthday: profile!.birthday,
|
birthday: profile!.birthday,
|
||||||
|
listenbrainz: profile!.listenbrainz,
|
||||||
lang: profile!.lang,
|
lang: profile!.lang,
|
||||||
fields: profile!.fields,
|
fields: profile!.fields,
|
||||||
verifiedLinks: profile!.verifiedLinks,
|
verifiedLinks: profile!.verifiedLinks,
|
||||||
|
|
|
@ -307,4 +307,5 @@ export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
||||||
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
|
export const listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } as const;
|
||||||
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||||
|
|
|
@ -37,6 +37,13 @@ export class MiUserProfile {
|
||||||
})
|
})
|
||||||
public birthday: string | null;
|
public birthday: string | null;
|
||||||
|
|
||||||
|
@Column("varchar", {
|
||||||
|
length: 128,
|
||||||
|
nullable: true,
|
||||||
|
comment: "The ListenBrainz username of the User.",
|
||||||
|
})
|
||||||
|
public listenbrainz: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 2048, nullable: true,
|
length: 2048, nullable: true,
|
||||||
comment: 'The description (bio) of the User.',
|
comment: 'The description (bio) of the User.',
|
||||||
|
|
|
@ -268,6 +268,12 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
example: '2018-03-12',
|
example: '2018-03-12',
|
||||||
},
|
},
|
||||||
|
ListenBrainz: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
optional: false,
|
||||||
|
example: "Steve",
|
||||||
|
},
|
||||||
lang: {
|
lang: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
import { birthdaySchema, listenbrainzSchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
||||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { langmap } from '@/misc/langmap.js';
|
import { langmap } from '@/misc/langmap.js';
|
||||||
|
@ -150,6 +150,7 @@ export const paramDef = {
|
||||||
description: { ...descriptionSchema, nullable: true },
|
description: { ...descriptionSchema, nullable: true },
|
||||||
location: { ...locationSchema, nullable: true },
|
location: { ...locationSchema, nullable: true },
|
||||||
birthday: { ...birthdaySchema, nullable: true },
|
birthday: { ...birthdaySchema, nullable: true },
|
||||||
|
listenbrainz: { ...listenbrainzSchema, nullable: true },
|
||||||
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 },
|
||||||
avatarDecorations: { type: 'array', maxItems: 16, items: {
|
avatarDecorations: { type: 'array', maxItems: 16, items: {
|
||||||
|
@ -306,6 +307,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
|
if (ps.listenbrainz !== undefined) profileUpdates.listenbrainz = ps.listenbrainz;
|
||||||
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
|
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
|
||||||
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
|
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
|
||||||
if (ps.mutedWords !== undefined) {
|
if (ps.mutedWords !== undefined) {
|
||||||
|
|
|
@ -37,6 +37,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #prefix><i class="ti ti-cake"></i></template>
|
<template #prefix><i class="ti ti-cake"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
<MkInput v-model="profile.listenbrainz" manualSave>
|
||||||
|
<template #label>ListenBrainz</template>
|
||||||
|
<template #caption>{{i18n.ts.listenbrainzDescription}}</template>
|
||||||
|
<template #prefix><i class="ti ti-headphones"></i></template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
<MkSelect v-model="profile.lang">
|
<MkSelect v-model="profile.lang">
|
||||||
<template #label>{{ i18n.ts.language }}</template>
|
<template #label>{{ i18n.ts.language }}</template>
|
||||||
<option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
|
<option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
|
||||||
|
@ -217,6 +223,7 @@ const profile = reactive({
|
||||||
description: $i.description,
|
description: $i.description,
|
||||||
location: $i.location,
|
location: $i.location,
|
||||||
birthday: $i.birthday,
|
birthday: $i.birthday,
|
||||||
|
listenbrainz: $i?.listenbrainz,
|
||||||
lang: $i.lang,
|
lang: $i.lang,
|
||||||
isBot: $i.isBot ?? false,
|
isBot: $i.isBot ?? false,
|
||||||
isCat: $i.isCat ?? false,
|
isCat: $i.isCat ?? false,
|
||||||
|
@ -300,6 +307,7 @@ function save() {
|
||||||
location: profile.location || null,
|
location: profile.location || null,
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
birthday: profile.birthday || null,
|
birthday: profile.birthday || null,
|
||||||
|
listenbrainz: profile.listenbrainz || null,
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
lang: profile.lang || null,
|
lang: profile.lang || null,
|
||||||
isBot: !!profile.isBot,
|
isBot: !!profile.isBot,
|
||||||
|
|
|
@ -209,6 +209,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
||||||
<XFiles :key="user.id" :user="user"/>
|
<XFiles :key="user.id" :user="user"/>
|
||||||
<XActivity :key="user.id" :user="user"/>
|
<XActivity :key="user.id" :user="user"/>
|
||||||
|
<XListenBrainz
|
||||||
|
v-if="user.listenbrainz && listenbrainzdata"
|
||||||
|
:key="user.id"
|
||||||
|
:user="user"
|
||||||
|
style="margin-top: var(--margin)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -258,6 +264,7 @@ function calcAge(birthdate: string): number {
|
||||||
|
|
||||||
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
|
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
|
||||||
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
|
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
|
||||||
|
const XListenBrainz = defineAsyncComponent(() => import("./index.listenbrainz.vue"));
|
||||||
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
@ -283,6 +290,24 @@ const moderationNote = ref(props.user.moderationNote);
|
||||||
const editModerationNote = ref(false);
|
const editModerationNote = ref(false);
|
||||||
const movedFromLog = ref<null | {movedFromId:string;}[]>(null);
|
const movedFromLog = ref<null | {movedFromId:string;}[]>(null);
|
||||||
|
|
||||||
|
let listenbrainzdata = false;
|
||||||
|
if (props.user.listenbrainz) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.payload.listens && data.payload.listens.length !== 0) {
|
||||||
|
listenbrainzdata = true;
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
listenbrainzdata = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(moderationNote, async () => {
|
watch(moderationNote, async () => {
|
||||||
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
|
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
|
||||||
});
|
});
|
||||||
|
|
140
packages/frontend/src/pages/user/index.listenbrainz.vue
Normal file
140
packages/frontend/src/pages/user/index.listenbrainz.vue
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: amelia and other Sharkey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkContainer :foldable="true" :expanded="!collapsed">
|
||||||
|
<template #header>
|
||||||
|
<i
|
||||||
|
class="ph-headphones ph-bold ph-lg"
|
||||||
|
style="margin-right: 0.5em"
|
||||||
|
></i>Music
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="padding: 8px">
|
||||||
|
<div class="flex">
|
||||||
|
<a :href="listenbrainz.musicbrainzurl">
|
||||||
|
<img class="image" :src="listenbrainz.img" :alt="listenbrainz.title"/>
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<p class="text-sm font-bold">Now Playing: {{ listenbrainz.title }}</p>
|
||||||
|
<p class="text-xs font-medium">{{ listenbrainz.artist }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a :href="listenbrainz.listenbrainzurl">
|
||||||
|
<div class="playicon">
|
||||||
|
<i class="ph-play ph-bold ph-lg"></i>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
user: misskey.entities.UserDetailed;
|
||||||
|
collapsed?: boolean;
|
||||||
|
}>(), {
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const listenbrainz = { title: '', artist: '', lastlisten: '', img: '', musicbrainzurl: '', listenbrainzurl: '' };
|
||||||
|
if (props.user.listenbrainz) {
|
||||||
|
const getLMData = async (title: string, artist: string) => {
|
||||||
|
const response = await fetch(`https://api.listenbrainz.org/1/metadata/lookup/?artist_name=${artist}&recording_name=${title}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.recording_name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const titler: string = data.recording_name;
|
||||||
|
const artistr: string = data.artist_credit_name;
|
||||||
|
const img: string = data.release_mbid ? `https://coverartarchive.org/release/${data.release_mbid}/front-250` : 'https://coverartarchive.org/img/big_logo.svg';
|
||||||
|
const musicbrainzurl: string = data.recording_mbid ? `https://musicbrainz.org/recording/${data.recording_mbid}` : '#';
|
||||||
|
const listenbrainzurl: string = data.recording_mbid ? `https://listenbrainz.org/player?recording_mbids=${data.recording_mbid}` : '#';
|
||||||
|
return [titler, artistr, img, musicbrainzurl, listenbrainzurl];
|
||||||
|
};
|
||||||
|
const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.payload.listens && data.payload.listens.length !== 0) {
|
||||||
|
const title: string = data.payload.listens[0].track_metadata.track_name;
|
||||||
|
const artist: string = data.payload.listens[0].track_metadata.artist_name;
|
||||||
|
const lastlisten: string = data.payload.listens[0].playing_now;
|
||||||
|
const img = 'https://coverartarchive.org/img/big_logo.svg';
|
||||||
|
await getLMData(title, artist).then((lmData) => {
|
||||||
|
if (!lmData) {
|
||||||
|
listenbrainz.title = title;
|
||||||
|
listenbrainz.img = img;
|
||||||
|
listenbrainz.artist = artist;
|
||||||
|
listenbrainz.lastlisten = lastlisten;
|
||||||
|
} else {
|
||||||
|
listenbrainz.title = lmData[0];
|
||||||
|
listenbrainz.img = lmData[2];
|
||||||
|
listenbrainz.artist = lmData[1];
|
||||||
|
listenbrainz.lastlisten = lastlisten;
|
||||||
|
listenbrainz.musicbrainzurl = lmData[3];
|
||||||
|
listenbrainz.listenbrainzurl = lmData[4];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.flex a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
height: 4.8rem;
|
||||||
|
margin-right: 0.7rem;
|
||||||
|
}
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.font-medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.playicon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
padding-left: 3rem;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue