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." | ||||
| operationForbidden: "Operation forbidden" | ||||
| forceShowAds: "Always show ads" | ||||
| oneko: "Cat friend :3" | ||||
| addMemo: "Add memo" | ||||
| editMemo: "Edit memo" | ||||
| 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; | ||||
|     /** | ||||
|      * 猫友達 :3 | ||||
|      */ | ||||
|     "oneko": string; | ||||
|     /** | ||||
|      * メモを追加 | ||||
|      */ | ||||
|  |  | |||
|  | @ -1102,6 +1102,7 @@ accountMoved: "このユーザーは新しいアカウントに移行しまし | |||
| accountMovedShort: "このアカウントは移行されています" | ||||
| operationForbidden: "この操作はできません" | ||||
| forceShowAds: "常に広告を表示する" | ||||
| oneko: "猫友達 :3" | ||||
| addMemo: "メモを追加" | ||||
| editMemo: "メモを編集" | ||||
| 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="disableDrawer">{{ i18n.ts.disableDrawer }}</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> | ||||
| 			</div> | ||||
| 			<div> | ||||
|  | @ -332,6 +333,7 @@ const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); | |||
| const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); | ||||
| const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); | ||||
| const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds')); | ||||
| const oneko = computed(defaultStore.makeGetterSetter('oneko')); | ||||
| const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); | ||||
| const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia')); | ||||
| const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); | ||||
|  |  | |||
|  | @ -98,6 +98,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ | |||
| 	'showClipButtonInNoteFooter', | ||||
| 	'reactionsDisplaySize', | ||||
| 	'forceShowAds', | ||||
| 	'oneko', | ||||
| 	'numberOfReplies', | ||||
| 	'aiChanMode', | ||||
| 	'devMode', | ||||
|  |  | |||
|  | @ -407,6 +407,10 @@ export const defaultStore = markRaw(new Storage('base', { | |||
| 		where: 'device', | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	oneko: { | ||||
| 		where: 'device', | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	clickToOpen: { | ||||
| 		where: 'device', | ||||
| 		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="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div> | ||||
| 
 | ||||
| <SkOneko v-if="defaultStore.state.oneko"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -59,6 +61,8 @@ import { i18n } from '@/i18n.js'; | |||
| import { defaultStore } from '@/store.js'; | ||||
| import { globalEvents } from '@/events.js'; | ||||
| 
 | ||||
| const SkOneko = defineAsyncComponent(() => import('@/components/SkOneko.vue')); | ||||
| 
 | ||||
| const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue')); | ||||
| const XUpload = defineAsyncComponent(() => import('./upload.vue')); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue