mirror of
https://github.com/sim1222/misskey.git
synced 2025-08-05 16:24:41 +09:00
Merge branch 'develop' into emoji-re
This commit is contained in:
@ -4,11 +4,14 @@
|
||||
<div style="overflow: clip;">
|
||||
<MkSpacer :content-max="600" :margin-min="20">
|
||||
<div class="_gaps_m znqjceqz">
|
||||
<div ref="containerEl" v-panel class="about" :class="{ playing: easterEggEngine != null }">
|
||||
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
|
||||
<div class="misskey">Misskey</div>
|
||||
<div class="version">v{{ version }}</div>
|
||||
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :is-reaction="false" :normal="true" :no-style="true"/></span>
|
||||
<div v-panel class="about">
|
||||
<div ref="containerEl" class="container" :class="{ playing: easterEggEngine != null }">
|
||||
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
|
||||
<div class="misskey">Misskey</div>
|
||||
<div class="version">v{{ version }}</div>
|
||||
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :is-reaction="false" :normal="true" :no-style="true"/></span>
|
||||
</div>
|
||||
<button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
|
||||
@ -70,6 +73,8 @@ import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const patrons = [
|
||||
'まっちゃとーにゅ',
|
||||
@ -152,6 +157,8 @@ const patrons = [
|
||||
'pixeldesu',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
||||
let easterEggReady = false;
|
||||
let easterEggEmojis = $ref([]);
|
||||
let easterEggEngine = $ref(null);
|
||||
@ -187,6 +194,11 @@ function iLoveMisskey() {
|
||||
});
|
||||
}
|
||||
|
||||
function getTreasure() {
|
||||
thereIsTreasure = false;
|
||||
claimAchievement('foundTreasure');
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (easterEggEngine) {
|
||||
easterEggEngine.stop();
|
||||
@ -207,52 +219,77 @@ definePageMetadata({
|
||||
.znqjceqz {
|
||||
> .about {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
|
||||
&.playing {
|
||||
&, * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
* {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
> .emoji {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
> .icon {
|
||||
display: block;
|
||||
width: 80px;
|
||||
margin: 0 auto;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
> .misskey {
|
||||
margin: 0.75em auto 0 auto;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
> .version {
|
||||
margin: 0 auto;
|
||||
width: max-content;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
> .emoji {
|
||||
> .treasure {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: min-content;
|
||||
|
||||
> .treasureImg {
|
||||
width: 25px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
> .container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
|
||||
&.playing {
|
||||
&, * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
* {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
> .emoji {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
> .icon {
|
||||
display: block;
|
||||
width: 80px;
|
||||
margin: 0 auto;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> .misskey {
|
||||
margin: 0.75em auto 0 auto;
|
||||
width: max-content;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> .version {
|
||||
margin: 0 auto;
|
||||
width: max-content;
|
||||
opacity: 0.5;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> .emoji {
|
||||
pointer-events: none;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
|
||||
> .emoji {
|
||||
pointer-events: none;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
import XFederation from './about.federation.vue';
|
||||
import { version, instanceName, host } from '@/config';
|
||||
@ -100,6 +100,7 @@ import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialTab?: string;
|
||||
@ -110,6 +111,12 @@ const props = withDefaults(defineProps<{
|
||||
let stats = $ref(null);
|
||||
let tab = $ref(props.initialTab);
|
||||
|
||||
watch($$(tab), () => {
|
||||
if (tab === 'charts') {
|
||||
claimAchievement('viewInstanceChart');
|
||||
}
|
||||
});
|
||||
|
||||
const initStats = () => os.api('stats', {
|
||||
}).then((res) => {
|
||||
stats = res;
|
||||
|
54
packages/frontend/src/pages/achievements.vue
Normal file
54
packages/frontend/src/pages/achievements.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="1200">
|
||||
<MkAchievements :user="$i"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
|
||||
import MkAchievements from '@/components/MkAchievements.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { $i } from '@/account';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
|
||||
let timer: number | null;
|
||||
|
||||
function viewAchievements3min() {
|
||||
claimAchievement('viewAchievements3min');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (timer == null) timer = window.setTimeout(viewAchievements3min, 1000 * 60 * 3);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer != null) {
|
||||
window.clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
if (timer == null) timer = window.setTimeout(viewAchievements3min, 1000 * 60 * 3);
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
if (timer != null) {
|
||||
window.clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.achievements,
|
||||
icon: 'ti ti-military-award',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
@ -7,7 +7,7 @@
|
||||
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role.baseRole }}</template>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps_s">
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
|
||||
<template #suffix>{{ Math.floor(policies.rateLimitFactor * 100) }}%</template>
|
||||
|
@ -16,6 +16,7 @@
|
||||
<div class="_buttons">
|
||||
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
|
||||
<MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
@ -94,6 +95,85 @@ Ui:render([
|
||||
])
|
||||
`;
|
||||
|
||||
const PRESET_SHUFFLE = `/// @ 0.12.2
|
||||
// 巻き戻し可能な文字シャッフルのプリセット
|
||||
|
||||
let string = "ペペロンチーノ"
|
||||
let length = string.len
|
||||
|
||||
// 過去の結果を保存しておくやつ
|
||||
var results = []
|
||||
|
||||
// どれだけ巻き戻しているか
|
||||
var cursor = 0
|
||||
|
||||
@do() {
|
||||
if (cursor != 0) {
|
||||
results = results.slice(0 (cursor + 1))
|
||||
cursor = 0
|
||||
}
|
||||
|
||||
let chars = []
|
||||
for (let i, length) {
|
||||
let r = Math:rnd(0 (length - 1))
|
||||
chars.push(string.pick(r))
|
||||
}
|
||||
let result = chars.join("")
|
||||
|
||||
results.push(result)
|
||||
|
||||
// UIを表示
|
||||
render(result)
|
||||
}
|
||||
|
||||
@back() {
|
||||
cursor = cursor + 1
|
||||
let result = results[results.len - (cursor + 1)]
|
||||
render(result)
|
||||
}
|
||||
|
||||
@forward() {
|
||||
cursor = cursor - 1
|
||||
let result = results[results.len - (cursor + 1)]
|
||||
render(result)
|
||||
}
|
||||
|
||||
@render(result) {
|
||||
Ui:render([
|
||||
Ui:C:container({
|
||||
align: 'center'
|
||||
children: [
|
||||
Ui:C:mfm({ text: result })
|
||||
Ui:C:buttons({
|
||||
buttons: [{
|
||||
text: "←"
|
||||
disabled: !(results.len > 1 && (results.len - cursor) > 1)
|
||||
onClick: back
|
||||
} {
|
||||
text: "→"
|
||||
disabled: !(results.len > 1 && cursor > 0)
|
||||
onClick: forward
|
||||
} {
|
||||
text: "引き直す"
|
||||
onClick: do
|
||||
}]
|
||||
})
|
||||
Ui:C:postFormButton({
|
||||
text: "投稿する"
|
||||
rounded: true
|
||||
primary: true
|
||||
form: {
|
||||
text: \`{result}{Str:lf}{THIS_URL}\`
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
do()
|
||||
`;
|
||||
|
||||
const PRESET_TIMELINE = `/// @ 0.12.2
|
||||
// APIリクエストを行いローカルタイムラインを表示するプリセット
|
||||
|
||||
@ -174,6 +254,11 @@ function selectPreset(ev: MouseEvent) {
|
||||
action: () => {
|
||||
script = PRESET_OMIKUJI;
|
||||
},
|
||||
}, {
|
||||
text: 'Shuffle',
|
||||
action: () => {
|
||||
script = PRESET_SHUFFLE;
|
||||
},
|
||||
}, {
|
||||
text: 'Timeline viewer',
|
||||
action: () => {
|
||||
@ -212,6 +297,19 @@ function show() {
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: flash.title }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('flash/delete', {
|
||||
flashId: props.id,
|
||||
});
|
||||
router.push('/play');
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
@ -11,12 +11,14 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'my'" class="my">
|
||||
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
<div class="_gaps">
|
||||
<MkButton class="new" gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'liked'" class="">
|
||||
|
@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkPageHeader />
|
||||
</template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="root"
|
||||
:class="$style['root']"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@drop.prevent.stop="onDrop"
|
||||
>
|
||||
<div class="body">
|
||||
<div :class="$style['body']">
|
||||
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
@ -17,7 +21,7 @@
|
||||
<MkDateSeparatedList
|
||||
v-if="messages.length > 0"
|
||||
v-slot="{ item: message }"
|
||||
:class="{ messages: true, 'deny-move-transition': pFetching }"
|
||||
:class="{ [$style['messages']]: true, 'deny-move-transition': pFetching }"
|
||||
:items="messages"
|
||||
direction="up"
|
||||
reversed
|
||||
@ -27,23 +31,26 @@
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<footer>
|
||||
<div v-if="typers.length > 0" class="typers">
|
||||
<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
|
||||
<footer :class="$style['footer']">
|
||||
<div v-if="typers.length > 0" :class="$style['typers']">
|
||||
<I18n :src="i18n.ts.typingUsers" text-tag="span">
|
||||
<template #users>
|
||||
<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b>
|
||||
<b v-for="typer in typers" :key="typer.id" :class="$style['user']">{{ typer.username }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkEllipsis/>
|
||||
</div>
|
||||
<Transition :name="animation ? 'fade' : ''">
|
||||
<div v-show="showIndicator" class="new-message">
|
||||
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button>
|
||||
<div v-show="showIndicator" :class="$style['new-message']">
|
||||
<button class="_buttonPrimary" @click="onIndicatorClick" :class="$style['new-message-button']">
|
||||
<i class="fas ti-fw fa-arrow-circle-down" :class="$style['new-message-icon']"></i>{{ i18n.ts.newMessageExists }}
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/>
|
||||
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" :class="$style['form']"/>
|
||||
</footer>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -303,103 +310,98 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: content;
|
||||
}
|
||||
|
||||
> .body {
|
||||
min-height: 80%;
|
||||
.body {
|
||||
min-height: 80%;
|
||||
}
|
||||
|
||||
.more {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
padding: 0 12px;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
background: rgba(#000, 0.3);
|
||||
border-radius: 12px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(#000, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(#000, 0.5);
|
||||
}
|
||||
|
||||
&.fetching {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 8px 0;
|
||||
|
||||
> ::v-deep(*) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
padding: 0 12px;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
background: rgba(#000, 0.3);
|
||||
border-radius: 12px;
|
||||
&:hover {
|
||||
background: rgba(#000, 0.4);
|
||||
}
|
||||
|
||||
> footer {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
padding-top: 8px;
|
||||
bottom: 0;
|
||||
bottom: env(safe-area-inset-bottom, 0px);
|
||||
|
||||
> .new-message {
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
text-align: center;
|
||||
|
||||
> button {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
border-radius: 16px;
|
||||
|
||||
> i {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .typers {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
padding: 0 8px 0 8px;
|
||||
font-size: 0.9em;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
> .users {
|
||||
> .user + .user:before {
|
||||
content: ", ";
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
> .user:last-of-type:after {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .form {
|
||||
max-height: 12em;
|
||||
overflow-y: scroll;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
&:active {
|
||||
background: rgba(#000, 0.5);
|
||||
}
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.fetching {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 16px 0 0;
|
||||
|
||||
> * {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
padding-top: 8px;
|
||||
bottom: var(--minBottomSpacing);
|
||||
}
|
||||
|
||||
.new-message {
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.new-message-button {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.new-message-icon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.typers {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
padding: 0 8px 0 8px;
|
||||
font-size: 0.9em;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
|
||||
|
||||
.user + .user:before {
|
||||
content: ", ";
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.user:last-of-type:after {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
.form {
|
||||
max-height: 12em;
|
||||
overflow-y: scroll;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
|
@ -47,6 +47,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
|
||||
import MkAsUi from '@/components/MkAsUi.vue';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
|
||||
const parser = new Parser();
|
||||
let aiscript: Interpreter;
|
||||
@ -90,6 +91,9 @@ async function run() {
|
||||
});
|
||||
},
|
||||
out: (value) => {
|
||||
if (value.type === 'str' && value.value.toLowerCase().replace(',', '').includes('hello world')) {
|
||||
claimAchievement('outputHelloWorldOnScratchpad');
|
||||
}
|
||||
logs.value.push({
|
||||
id: Math.random(),
|
||||
text: value.type === 'str' ? value.value : utils.valToString(value),
|
||||
|
@ -85,6 +85,7 @@ import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
import { langmap } from '@/scripts/langmap';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
|
||||
const profile = reactive({
|
||||
name: $i.name,
|
||||
@ -133,6 +134,13 @@ function save() {
|
||||
isCat: !!profile.isCat,
|
||||
showTimelineReplies: !!profile.showTimelineReplies,
|
||||
});
|
||||
claimAchievement('profileFilled');
|
||||
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
|
||||
claimAchievement('setNameToSyuilo');
|
||||
}
|
||||
if (profile.isCat) {
|
||||
claimAchievement('markedAsCat');
|
||||
}
|
||||
}
|
||||
|
||||
function changeAvatar(ev) {
|
||||
@ -155,6 +163,7 @@ function changeAvatar(ev) {
|
||||
});
|
||||
$i.avatarId = i.avatarId;
|
||||
$i.avatarUrl = i.avatarUrl;
|
||||
claimAchievement('profileFilled');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,8 @@
|
||||
|
||||
<div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
|
||||
<MkRolePreview :class="$style.role" :role="role"/>
|
||||
<button class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
52
packages/frontend/src/pages/user/achievements.vue
Normal file
52
packages/frontend/src/pages/user/achievements.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="1200">
|
||||
<MkAchievements :user="user" :with-locked="false"/>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkAchievements from '@/components/MkAchievements.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.User;
|
||||
}>();
|
||||
|
||||
let timer: number | null;
|
||||
|
||||
function viewAchievements3min() {
|
||||
if ($i && (props.user.id === $i.id)) {
|
||||
claimAchievement('viewAchievements3min');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (timer == null) timer = window.setTimeout(viewAchievements3min, 1000 * 60 * 3);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer != null) {
|
||||
window.clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
if (timer == null) timer = window.setTimeout(viewAchievements3min, 1000 * 60 * 3);
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
if (timer != null) {
|
||||
window.clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
@ -6,6 +6,7 @@
|
||||
<div v-if="user">
|
||||
<XHome v-if="tab === 'home'" :user="user"/>
|
||||
<XActivity v-else-if="tab === 'activity'" :user="user"/>
|
||||
<XAchievements v-else-if="tab === 'achievements'" :user="user"/>
|
||||
<XReactions v-else-if="tab === 'reactions'" :user="user"/>
|
||||
<XClips v-else-if="tab === 'clips'" :user="user"/>
|
||||
<XPages v-else-if="tab === 'pages'" :user="user"/>
|
||||
@ -34,6 +35,7 @@ import { $i } from '@/account';
|
||||
|
||||
const XHome = defineAsyncComponent(() => import('./home.vue'));
|
||||
const XActivity = defineAsyncComponent(() => import('./activity.vue'));
|
||||
const XAchievements = defineAsyncComponent(() => import('./achievements.vue'));
|
||||
const XReactions = defineAsyncComponent(() => import('./reactions.vue'));
|
||||
const XClips = defineAsyncComponent(() => import('./clips.vue'));
|
||||
const XPages = defineAsyncComponent(() => import('./pages.vue'));
|
||||
@ -76,7 +78,11 @@ const headerTabs = $computed(() => user ? [{
|
||||
key: 'activity',
|
||||
title: i18n.ts.activity,
|
||||
icon: 'ti ti-chart-line',
|
||||
}, ...($i && ($i.id === user.id)) || user.publicReactions ? [{
|
||||
}, ...(user.host == null ? [{
|
||||
key: 'achievements',
|
||||
title: i18n.ts.achievements,
|
||||
icon: 'ti ti-military-award',
|
||||
}] : []), ...($i && ($i.id === user.id)) || user.publicReactions ? [{
|
||||
key: 'reactions',
|
||||
title: i18n.ts.reaction,
|
||||
icon: 'ti ti-mood-happy',
|
||||
|
Reference in New Issue
Block a user