Merge branch 'oneko' into 'develop'
feat: oneko See merge request TransFem-org/Sharkey!387
This commit is contained in:
commit
ea44895b6b
@ -1095,6 +1095,7 @@ accountMoved: "This user has moved to a new account:"
|
|||||||
accountMovedShort: "This account has been migrated."
|
accountMovedShort: "This account has been migrated."
|
||||||
operationForbidden: "Operation forbidden"
|
operationForbidden: "Operation forbidden"
|
||||||
forceShowAds: "Always show ads"
|
forceShowAds: "Always show ads"
|
||||||
|
oneko: "Cat friend :3"
|
||||||
addMemo: "Add memo"
|
addMemo: "Add memo"
|
||||||
editMemo: "Edit memo"
|
editMemo: "Edit memo"
|
||||||
reactionsList: "Reactions"
|
reactionsList: "Reactions"
|
||||||
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@ -4425,6 +4425,10 @@ export interface Locale extends ILocale {
|
|||||||
* 常に広告を表示する
|
* 常に広告を表示する
|
||||||
*/
|
*/
|
||||||
"forceShowAds": string;
|
"forceShowAds": string;
|
||||||
|
/**
|
||||||
|
* 猫友達 :3
|
||||||
|
*/
|
||||||
|
"oneko": string;
|
||||||
/**
|
/**
|
||||||
* メモを追加
|
* メモを追加
|
||||||
*/
|
*/
|
||||||
|
@ -1102,6 +1102,7 @@ accountMoved: "このユーザーは新しいアカウントに移行しまし
|
|||||||
accountMovedShort: "このアカウントは移行されています"
|
accountMovedShort: "このアカウントは移行されています"
|
||||||
operationForbidden: "この操作はできません"
|
operationForbidden: "この操作はできません"
|
||||||
forceShowAds: "常に広告を表示する"
|
forceShowAds: "常に広告を表示する"
|
||||||
|
oneko: "猫友達 :3"
|
||||||
addMemo: "メモを追加"
|
addMemo: "メモを追加"
|
||||||
editMemo: "メモを編集"
|
editMemo: "メモを編集"
|
||||||
reactionsList: "リアクション一覧"
|
reactionsList: "リアクション一覧"
|
||||||
|
BIN
packages/frontend/assets/oneko.gif
Normal file
BIN
packages/frontend/assets/oneko.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
240
packages/frontend/src/components/SkOneko.vue
Normal file
240
packages/frontend/src/components/SkOneko.vue
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="nekoEl" :class="$style.oneko" aria-hidden="true"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// oneko.js: https://github.com/adryd325/oneko.js
|
||||||
|
// modified to be a vue component by ShittyKopper :3
|
||||||
|
|
||||||
|
import { shallowRef, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const nekoEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
let nekoPosX = 32;
|
||||||
|
let nekoPosY = 32;
|
||||||
|
|
||||||
|
let mousePosX = 0;
|
||||||
|
let mousePosY = 0;
|
||||||
|
|
||||||
|
let frameCount = 0;
|
||||||
|
let idleTime = 0;
|
||||||
|
let idleAnimation: string|null = null;
|
||||||
|
let idleAnimationFrame = 0;
|
||||||
|
let lastFrameTimestamp;
|
||||||
|
|
||||||
|
const nekoSpeed = 10;
|
||||||
|
const spriteSets = {
|
||||||
|
idle: [[-3, -3]],
|
||||||
|
alert: [[-7, -3]],
|
||||||
|
scratchSelf: [
|
||||||
|
[-5, 0],
|
||||||
|
[-6, 0],
|
||||||
|
[-7, 0],
|
||||||
|
],
|
||||||
|
scratchWallN: [
|
||||||
|
[0, 0],
|
||||||
|
[0, -1],
|
||||||
|
],
|
||||||
|
scratchWallS: [
|
||||||
|
[-7, -1],
|
||||||
|
[-6, -2],
|
||||||
|
],
|
||||||
|
scratchWallE: [
|
||||||
|
[-2, -2],
|
||||||
|
[-2, -3],
|
||||||
|
],
|
||||||
|
scratchWallW: [
|
||||||
|
[-4, 0],
|
||||||
|
[-4, -1],
|
||||||
|
],
|
||||||
|
tired: [[-3, -2]],
|
||||||
|
sleeping: [
|
||||||
|
[-2, 0],
|
||||||
|
[-2, -1],
|
||||||
|
],
|
||||||
|
N: [
|
||||||
|
[-1, -2],
|
||||||
|
[-1, -3],
|
||||||
|
],
|
||||||
|
NE: [
|
||||||
|
[0, -2],
|
||||||
|
[0, -3],
|
||||||
|
],
|
||||||
|
E: [
|
||||||
|
[-3, 0],
|
||||||
|
[-3, -1],
|
||||||
|
],
|
||||||
|
SE: [
|
||||||
|
[-5, -1],
|
||||||
|
[-5, -2],
|
||||||
|
],
|
||||||
|
S: [
|
||||||
|
[-6, -3],
|
||||||
|
[-7, -2],
|
||||||
|
],
|
||||||
|
SW: [
|
||||||
|
[-5, -3],
|
||||||
|
[-6, -1],
|
||||||
|
],
|
||||||
|
W: [
|
||||||
|
[-4, -2],
|
||||||
|
[-4, -3],
|
||||||
|
],
|
||||||
|
NW: [
|
||||||
|
[-1, 0],
|
||||||
|
[-1, -1],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (!nekoEl.value) return;
|
||||||
|
|
||||||
|
nekoEl.value.style.left = `${nekoPosX - 16}px`;
|
||||||
|
nekoEl.value.style.top = `${nekoPosY - 16}px`;
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (event) => {
|
||||||
|
mousePosX = event.clientX;
|
||||||
|
mousePosY = event.clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.requestAnimationFrame(onAnimationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAnimationFrame(timestamp) {
|
||||||
|
// Stops execution if the neko element is removed from DOM
|
||||||
|
if (!nekoEl.value?.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!lastFrameTimestamp) {
|
||||||
|
lastFrameTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
if (timestamp - lastFrameTimestamp > 100) {
|
||||||
|
lastFrameTimestamp = timestamp;
|
||||||
|
frame();
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(onAnimationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
function setSprite(name, frame) {
|
||||||
|
if (!nekoEl.value) return;
|
||||||
|
|
||||||
|
const sprite = spriteSets[name][frame % spriteSets[name].length];
|
||||||
|
nekoEl.value.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetIdleAnimation() {
|
||||||
|
idleAnimation = null;
|
||||||
|
idleAnimationFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function idle() {
|
||||||
|
idleTime += 1;
|
||||||
|
|
||||||
|
// every ~ 20 seconds
|
||||||
|
if (
|
||||||
|
idleTime > 10 &&
|
||||||
|
Math.floor(Math.random() * 200) === 0 &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
idleAnimation == null
|
||||||
|
) {
|
||||||
|
let avalibleIdleAnimations = ['sleeping', 'scratchSelf'];
|
||||||
|
if (nekoPosX < 32) {
|
||||||
|
avalibleIdleAnimations.push('scratchWallW');
|
||||||
|
}
|
||||||
|
if (nekoPosY < 32) {
|
||||||
|
avalibleIdleAnimations.push('scratchWallN');
|
||||||
|
}
|
||||||
|
if (nekoPosX > window.innerWidth - 32) {
|
||||||
|
avalibleIdleAnimations.push('scratchWallE');
|
||||||
|
}
|
||||||
|
if (nekoPosY > window.innerHeight - 32) {
|
||||||
|
avalibleIdleAnimations.push('scratchWallS');
|
||||||
|
}
|
||||||
|
idleAnimation =
|
||||||
|
avalibleIdleAnimations[Math.floor(Math.random() * avalibleIdleAnimations.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (idleAnimation) {
|
||||||
|
case 'sleeping':
|
||||||
|
if (idleAnimationFrame < 8) {
|
||||||
|
setSprite('tired', 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setSprite('sleeping', Math.floor(idleAnimationFrame / 4));
|
||||||
|
if (idleAnimationFrame > 192) {
|
||||||
|
resetIdleAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'scratchWallN':
|
||||||
|
case 'scratchWallS':
|
||||||
|
case 'scratchWallE':
|
||||||
|
case 'scratchWallW':
|
||||||
|
case 'scratchSelf':
|
||||||
|
setSprite(idleAnimation, idleAnimationFrame);
|
||||||
|
if (idleAnimationFrame > 9) {
|
||||||
|
resetIdleAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setSprite('idle', 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
idleAnimationFrame += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function frame() {
|
||||||
|
if (!nekoEl.value) return;
|
||||||
|
|
||||||
|
frameCount += 1;
|
||||||
|
const diffX = nekoPosX - mousePosX;
|
||||||
|
const diffY = nekoPosY - mousePosY;
|
||||||
|
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
|
||||||
|
|
||||||
|
if (distance < nekoSpeed || distance < 48) {
|
||||||
|
idle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
idleAnimation = null;
|
||||||
|
idleAnimationFrame = 0;
|
||||||
|
|
||||||
|
if (idleTime > 1) {
|
||||||
|
setSprite('alert', 0);
|
||||||
|
// count down after being alerted before moving
|
||||||
|
idleTime = Math.min(idleTime, 7);
|
||||||
|
idleTime -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction;
|
||||||
|
direction = diffY / distance > 0.5 ? 'N' : '';
|
||||||
|
direction += diffY / distance < -0.5 ? 'S' : '';
|
||||||
|
direction += diffX / distance > 0.5 ? 'W' : '';
|
||||||
|
direction += diffX / distance < -0.5 ? 'E' : '';
|
||||||
|
setSprite(direction, frameCount);
|
||||||
|
|
||||||
|
nekoPosX -= (diffX / distance) * nekoSpeed;
|
||||||
|
nekoPosY -= (diffY / distance) * nekoSpeed;
|
||||||
|
|
||||||
|
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
|
||||||
|
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
|
||||||
|
|
||||||
|
nekoEl.value.style.left = `${nekoPosX - 16}px`;
|
||||||
|
nekoEl.value.style.top = `${nekoPosY - 16}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(init);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.oneko {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
z-index: 2147483647;
|
||||||
|
background-image: url(/client-assets/oneko.gif);
|
||||||
|
}
|
||||||
|
</style>
|
@ -145,6 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
|
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
|
||||||
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
|
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
|
||||||
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
|
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="oneko">{{ i18n.ts.oneko }}</MkSwitch>
|
||||||
<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
|
<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -332,6 +333,7 @@ const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
|
|||||||
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
|
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
|
||||||
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
|
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
|
||||||
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
|
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
|
||||||
|
const oneko = computed(defaultStore.makeGetterSetter('oneko'));
|
||||||
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
|
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
|
||||||
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
|
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
|
||||||
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
|
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
|
||||||
|
@ -98,6 +98,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
|||||||
'showClipButtonInNoteFooter',
|
'showClipButtonInNoteFooter',
|
||||||
'reactionsDisplaySize',
|
'reactionsDisplaySize',
|
||||||
'forceShowAds',
|
'forceShowAds',
|
||||||
|
'oneko',
|
||||||
'numberOfReplies',
|
'numberOfReplies',
|
||||||
'aiChanMode',
|
'aiChanMode',
|
||||||
'devMode',
|
'devMode',
|
||||||
|
@ -407,6 +407,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
oneko: {
|
||||||
|
where: 'device',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
clickToOpen: {
|
clickToOpen: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -42,6 +42,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
||||||
|
|
||||||
<div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div>
|
<div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div>
|
||||||
|
|
||||||
|
<SkOneko v-if="defaultStore.state.oneko"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -59,6 +61,8 @@ import { i18n } from '@/i18n.js';
|
|||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
|
|
||||||
|
const SkOneko = defineAsyncComponent(() => import('@/components/SkOneko.vue'));
|
||||||
|
|
||||||
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
||||||
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user