mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-30 21:14:12 +00:00 
			
		
		
		
	Merge branch 'oneko' into 'develop'
feat: oneko See merge request TransFem-org/Sharkey!387
This commit is contained in:
		
						commit
						ea44895b6b
					
				
					 9 changed files with 257 additions and 0 deletions
				
			
		|  | @ -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…
	
	Add table
		
		Reference in a new issue