From 43d0bd29e42a83616934118a482b058428ad777e Mon Sep 17 00:00:00 2001 From: clragon Date: Sat, 18 May 2024 00:00:00 +0200 Subject: [PATCH 01/14] undid walltaker changes to prevent merge conflict --- src/home/HomePage.tsx | 4 +- .../{WallTakerAd.tsx => WallTalkerAd.tsx} | 37 +++++++++++-------- src/home/components/index.ts | 3 +- src/settings/components/ServiceSettings.tsx | 6 +-- ...kerSettings.tsx => WalltalkerSettings.tsx} | 2 +- src/settings/components/index.ts | 2 +- src/walltaker/index.ts | 1 - .../WalltalkerSearch.tsx} | 2 +- src/walltalker/index.ts | 1 + 9 files changed, 32 insertions(+), 26 deletions(-) rename src/home/components/{WallTakerAd.tsx => WallTalkerAd.tsx} (62%) rename src/settings/components/{WalltakerSettings.tsx => WalltalkerSettings.tsx} (93%) delete mode 100644 src/walltaker/index.ts rename src/{walltaker/WalltakerSearch.tsx => walltalker/WalltalkerSearch.tsx} (92%) create mode 100644 src/walltalker/index.ts diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index df13921d..ec1ea38f 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { HomeTitle, - WallTakerAd, + WallTalkerAd, ReleaseNotes, AgeWarning, Introduction, @@ -34,7 +34,7 @@ export const HomePage = () => { - + diff --git a/src/home/components/WallTakerAd.tsx b/src/home/components/WallTalkerAd.tsx similarity index 62% rename from src/home/components/WallTakerAd.tsx rename to src/home/components/WallTalkerAd.tsx index 57feea5e..d9e3f340 100644 --- a/src/home/components/WallTakerAd.tsx +++ b/src/home/components/WallTalkerAd.tsx @@ -2,7 +2,7 @@ import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { ContentSection } from '../../common'; -const StyledWallTakerAd = styled(ContentSection)` +const StyledWallTalkerAd = styled(ContentSection)` background: linear-gradient(#ffb300, #916706); color: #583c0f; @@ -27,24 +27,29 @@ const StyledWallTakerAd = styled(ContentSection)` & a { text-decoration: none; } -`; -const StyledWalltakerMascotBackground = styled.div` - bottom: -50px; - right: -40px; - height: 180px; - aspect-ratio: 1/1; - position: absolute; - background-image: url('https://walltaker.joi.how/assets/mascot/TaylorSFW-1f4700509acff90902c73b80246473840a4879dca17a0052e0d8a41b1e4556e2.png'); - background-repeat: no-repeat; - background-size: contain; - background-position: right bottom; + &::before { + position: absolute; + + content: 'Walltalker ✨'; + + background-color: #000000b5; + color: #e6e6e6; + text-align: center; + + right: -20px; + bottom: 120px; + width: 200px; + height: 20px; + + transform: translateY(50%) rotate(-45deg); + transform-origin: top right; + } `; -export const WallTakerAd = () => { +export const WallTalkerAd = () => { return ( - - +

Want to let other people set your wallpaper?

@@ -54,6 +59,6 @@ export const WallTakerAd = () => { within your blacklist!

-
+ ); }; diff --git a/src/home/components/index.ts b/src/home/components/index.ts index 6ff52b1b..aaae853f 100644 --- a/src/home/components/index.ts +++ b/src/home/components/index.ts @@ -1,8 +1,9 @@ export * from './AgeWarning'; export * from './AppTitle'; +export * from '../../common'; export * from './Instructions'; export * from './Introduction'; export * from './ReleaseNotes'; export * from './StartButton'; export * from './VersionDisplay'; -export * from './WallTakerAd'; +export * from './WallTalkerAd'; diff --git a/src/settings/components/ServiceSettings.tsx b/src/settings/components/ServiceSettings.tsx index 7f1894b3..bc66e642 100644 --- a/src/settings/components/ServiceSettings.tsx +++ b/src/settings/components/ServiceSettings.tsx @@ -2,12 +2,12 @@ import styled from 'styled-components'; import { SettingsTile, TabBar } from '../../common'; import { E621Search } from '../../e621'; import { useState } from 'react'; -import { WalltakerSearch } from '../../walltaker'; +import { WalltalkerSearch } from '../../walltalker'; import { LocalImport } from '../../local'; const tabs: Record = { e621: , - walltaker: , + walltalker: , local: , }; @@ -29,7 +29,7 @@ export const ServiceSettings = () => { { +export const WalltalkerSettings = () => { return (
{ +export const WalltalkerSearch = () => { return (
Date: Sun, 19 May 2024 13:46:14 -0400 Subject: [PATCH 02/14] add a service for the walltaker actioncable api. Could use a library for this, but it's a really simple protocol --- src/app/App.tsx | 15 ++- src/utils/porn-socket/porn-socket-service.tsx | 30 +++++ src/utils/porn-socket/walltaker.tsx | 106 ++++++++++++++++++ 3 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/utils/porn-socket/porn-socket-service.tsx create mode 100644 src/utils/porn-socket/walltaker.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index cf2fd360..561af047 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,14 +1,17 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { HomePage } from '../home'; import { GamePage } from '../game'; +import { WalltakerSocketServiceProvider } from '../utils/porn-socket/walltaker.tsx'; export const App = () => { return ( - - - } /> - } /> - - + + + + } /> + } /> + + + ); }; diff --git a/src/utils/porn-socket/porn-socket-service.tsx b/src/utils/porn-socket/porn-socket-service.tsx new file mode 100644 index 00000000..fff59621 --- /dev/null +++ b/src/utils/porn-socket/porn-socket-service.tsx @@ -0,0 +1,30 @@ +import { createContext, useContext } from 'use-context-selector'; +import { useEffect, useState } from 'react'; + +export interface PornSocketService { + connect(): Promise; + + disconnect(): Promise; + + listenTo(id: number): Promise; + + muteFrom(id: number): Promise; +} + +export const PornSocketContext = createContext(null); + +export function usePornSocketService() { + const service = useContext(PornSocketContext); + + const [ready, setReady] = useState(false); + + useEffect(() => { + service?.connect().then(() => setReady(true)) + }, [service]); + + return { + enabled: Boolean(service), + ready, + service, + }; +} \ No newline at end of file diff --git a/src/utils/porn-socket/walltaker.tsx b/src/utils/porn-socket/walltaker.tsx new file mode 100644 index 00000000..998af966 --- /dev/null +++ b/src/utils/porn-socket/walltaker.tsx @@ -0,0 +1,106 @@ +import { PropsWithChildren } from 'react'; +import { PornSocketContext, PornSocketService } from './porn-socket-service.tsx'; + +class WalltakerSocketService implements PornSocketService { + private socket: WebSocket | null = null; + + connect(): Promise { + return new Promise((res, rej) => { + if (this.socket) { + switch(this.socket.readyState) { + case this.socket.OPEN: + res(); + return; + case this.socket.CONNECTING: + this.socket.addEventListener('open', () => res()); + this.socket.addEventListener('error', () => rej()); + return; + case this.socket.CLOSING: + case this.socket.CLOSED: + this.socket = null; + return this.connect() + } + } + + const socket = new WebSocket('wss://walltaker.joi.how/cable'); + this.socket = socket; + + socket.addEventListener('open', () => res()); + socket.addEventListener('error', () => rej()); + }); + } + + disconnect(): Promise { + return new Promise((res) => { + if (this.socket) { + this.socket.addEventListener('close', () => res()); + this.socket.close(); + } + this.socket = null; + }); + } + + listenTo(id: number): Promise { + return new Promise((res, rej) => { + if (!this.socket) { + rej('No socket connected.'); + return; + } + + const channelId = this.channelIdentifierFor(id); + const message = { command: 'subscribe', identifier: channelId }; + + this.socket.addEventListener('message', this.firstMessageHandlerFor('confirm_subscription', id, rej, res)); + this.socket.send(JSON.stringify(message)); + }); + } + + muteFrom(id: number): Promise { + return new Promise((res, rej) => { + if (!this.socket) { + rej('No socket connected.'); + return; + } + + const channelId = this.channelIdentifierFor(id); + const message = { command: 'unsubscribe', identifier: channelId }; + + this.socket.send(JSON.stringify(message)); + res(); + }); + } + + private channelIdentifierFor(id: number) { + return JSON.stringify({ channel: 'LinkChannel', id }); + } + + private firstMessageHandlerFor(eventName: string, id: number, rej: (message?: string) => void, res: () => void) { + const MAX_RETRIES = 4; + let tries = 0; + + const handler = (message: MessageEvent) => { + const content = JSON.parse(message.data); + if (content.type === eventName && content.identifier === this.channelIdentifierFor(id)) { + this.socket?.removeEventListener('message', handler); + res(); + return; + } + + if (++tries > MAX_RETRIES) { + this.socket?.removeEventListener('message', handler); + rej(`Max retries hit when waiting for subscription confirmation for Link#${id}.`); + } + }; + + return handler; + } + + private makeSocket() { + } + +} + +export function WalltakerSocketServiceProvider({ children }: PropsWithChildren) { + const walltakerSocketService = new WalltakerSocketService(); + return {children}; +} \ No newline at end of file From 33bfd0821c4f2de68b7696d10b24496a258924e2 Mon Sep 17 00:00:00 2001 From: Pup Gray Date: Sun, 19 May 2024 14:51:42 -0400 Subject: [PATCH 03/14] set up a ui for connecting to the stream --- src/game/components/GameSettings.tsx | 2 + src/settings/SettingsProvider.tsx | 7 +- src/settings/SettingsSection.tsx | 2 + .../components/WalltalkerSettings.tsx | 66 ++++++++++++++----- src/types/index.ts | 1 + src/types/walltaker.ts | 4 ++ src/utils/porn-socket/porn-socket-service.tsx | 17 +++-- src/utils/porn-socket/walltaker.tsx | 10 +-- 8 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 src/types/walltaker.ts diff --git a/src/game/components/GameSettings.tsx b/src/game/components/GameSettings.tsx index d2967d72..18c0abfb 100644 --- a/src/game/components/GameSettings.tsx +++ b/src/game/components/GameSettings.tsx @@ -13,6 +13,7 @@ import { PaceSettings, PlayerSettings, VibratorSettings, + WalltalkerSettings, } from '../../settings'; import { GamePhase, useGameValue, useSendMessage } from '../GameProvider'; import { useFullscreen, useLooping } from '../../utils'; @@ -59,6 +60,7 @@ const GameSettingsDialog: React.FC = props => { + } diff --git a/src/settings/SettingsProvider.tsx b/src/settings/SettingsProvider.tsx index 22334c27..b48bf24e 100644 --- a/src/settings/SettingsProvider.tsx +++ b/src/settings/SettingsProvider.tsx @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { GameEvent, GameHypnoType, PlayerBody, PlayerGender } from '../types'; +import { GameEvent, GameHypnoType, PlayerBody, PlayerGender, WalltakerConfig } from '../types'; import { createLocalStorageProvider, VibrationMode } from '../utils'; import { interpolateWith } from '../utils/translate'; @@ -19,6 +19,7 @@ export interface Settings { highRes: boolean; videoSound: boolean; vibrations: VibrationMode; + walltaker: WalltakerConfig; } export const defaultSettings: Settings = { @@ -37,6 +38,10 @@ export const defaultSettings: Settings = { highRes: false, videoSound: false, vibrations: VibrationMode.thump, + walltaker: { + enabled: false, + id: null + } }; const settingsStorageKey = 'settings'; diff --git a/src/settings/SettingsSection.tsx b/src/settings/SettingsSection.tsx index 7193ce53..0ac3c508 100644 --- a/src/settings/SettingsSection.tsx +++ b/src/settings/SettingsSection.tsx @@ -12,6 +12,7 @@ import { BoardSettings, VibratorSettings, TradeSettings, + WalltalkerSettings, } from './components'; const StyledSettingsSection = styled(ContentSection)` @@ -32,6 +33,7 @@ export const SettingsSection = () => { + ); diff --git a/src/settings/components/WalltalkerSettings.tsx b/src/settings/components/WalltalkerSettings.tsx index 0ae781da..ca108b5d 100644 --- a/src/settings/components/WalltalkerSettings.tsx +++ b/src/settings/components/WalltalkerSettings.tsx @@ -1,25 +1,59 @@ +import { SettingsTile, ToggleTile, ToggleTileType } from '../../common'; +import { + faSpinner, + faSquare, + faCheckSquare, + faPowerOff, +} from '@fortawesome/free-solid-svg-icons'; +import { usePornSocketService } from '../../utils/porn-socket/porn-socket-service.tsx'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { SettingsTile } from '../../common'; -import { faPersonDigging } from '@fortawesome/free-solid-svg-icons'; +import styled from 'styled-components'; +import { useSetting } from '../SettingsProvider.tsx'; + +const Header = styled.div` + display: flex; + gap: 1ex; + align-items: center; +`; export const WalltalkerSettings = () => { + const [config, setConfig] = useSetting('walltaker'); + const service = usePornSocketService(config.enabled); + return ( - -
+ { + setConfig({ ...config, enabled: !service.enabled }); + service.setEnabled(!service.enabled); }} + type={ToggleTileType.radio} + trailing={service.enabled && !service.ready ? : } > -

- -

-

There is nothing here yet :3

-
+ Use Walltaker +

Let others choose wallpapers for your session, live!

+ +
+ {service.enabled && service.ready && ( + <> + + Ready! + + )} + {service.enabled && !service.ready && ( + <> + + Connecting... + + )} + {!service.enabled && ( + <> + + Disabled + + )} +
); }; diff --git a/src/types/index.ts b/src/types/index.ts index 007195a8..f0534e77 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,3 +3,4 @@ export * from './event'; export * from './gender'; export * from './hypno'; export * from './image'; +export * from './walltaker'; diff --git a/src/types/walltaker.ts b/src/types/walltaker.ts new file mode 100644 index 00000000..9e8a14cb --- /dev/null +++ b/src/types/walltaker.ts @@ -0,0 +1,4 @@ +export interface WalltakerConfig { + enabled: boolean; + id: number | null; +} diff --git a/src/utils/porn-socket/porn-socket-service.tsx b/src/utils/porn-socket/porn-socket-service.tsx index fff59621..8f90cc09 100644 --- a/src/utils/porn-socket/porn-socket-service.tsx +++ b/src/utils/porn-socket/porn-socket-service.tsx @@ -13,17 +13,26 @@ export interface PornSocketService { export const PornSocketContext = createContext(null); -export function usePornSocketService() { +export function usePornSocketService(enableDefault = false) { const service = useContext(PornSocketContext); + const [enabled, setEnabled] = useState(enableDefault); const [ready, setReady] = useState(false); useEffect(() => { - service?.connect().then(() => setReady(true)) - }, [service]); + if (enabled) { + service?.connect() + .then(() => setReady(true)) + .catch(() => setReady(false));} + + return () => { + service?.disconnect().then(() => setReady(false)); + }; + }, [service, enabled]); return { - enabled: Boolean(service), + enabled: Boolean(service) && enabled, + setEnabled, ready, service, }; diff --git a/src/utils/porn-socket/walltaker.tsx b/src/utils/porn-socket/walltaker.tsx index 998af966..16fd2293 100644 --- a/src/utils/porn-socket/walltaker.tsx +++ b/src/utils/porn-socket/walltaker.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, useRef } from 'react'; import { PornSocketContext, PornSocketService } from './porn-socket-service.tsx'; class WalltakerSocketService implements PornSocketService { @@ -94,13 +94,9 @@ class WalltakerSocketService implements PornSocketService { return handler; } - - private makeSocket() { - } - } export function WalltakerSocketServiceProvider({ children }: PropsWithChildren) { - const walltakerSocketService = new WalltakerSocketService(); - return {children}; + const walltakerSocketService = useRef(new WalltakerSocketService()); + return {children}; } \ No newline at end of file From 723f54d38dd35dc2a82c9a2d96e4d3125f61b08f Mon Sep 17 00:00:00 2001 From: Pup Gray Date: Sun, 19 May 2024 16:01:41 -0400 Subject: [PATCH 04/14] Finished the UI but this is a messsss --- .../components/WalltalkerSettings.tsx | 118 +++++++++++++++++- src/utils/porn-socket/walltaker.tsx | 42 ++++++- 2 files changed, 151 insertions(+), 9 deletions(-) diff --git a/src/settings/components/WalltalkerSettings.tsx b/src/settings/components/WalltalkerSettings.tsx index ca108b5d..febad572 100644 --- a/src/settings/components/WalltalkerSettings.tsx +++ b/src/settings/components/WalltalkerSettings.tsx @@ -1,52 +1,111 @@ -import { SettingsTile, ToggleTile, ToggleTileType } from '../../common'; +import { Button, SettingsLabel, SettingsTile, Spinner, TextInput, ToggleTile, ToggleTileType } from '../../common'; import { faSpinner, faSquare, faCheckSquare, - faPowerOff, + faPowerOff } from '@fortawesome/free-solid-svg-icons'; import { usePornSocketService } from '../../utils/porn-socket/porn-socket-service.tsx'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import styled from 'styled-components'; import { useSetting } from '../SettingsProvider.tsx'; +import { useEffect, useState } from 'react'; +import { LinkResponse, WalltakerSocketService } from '../../utils/porn-socket/walltaker.tsx'; +import { defaultTransition } from '../../utils'; +import { motion } from 'framer-motion'; const Header = styled.div` display: flex; gap: 1ex; align-items: center; + margin-bottom: 0.75rem; +`; + +const StyledLinksForm = styled.div` + grid-column: 1 / -1; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.75rem; `; export const WalltalkerSettings = () => { const [config, setConfig] = useSetting('walltaker'); const service = usePornSocketService(config.enabled); + const [username, setUsername] = useState(''); + const [loadingLinks, setLoadingLinks] = useState(false); + const [loadingLink, setLoadingLink] = useState(false); + const [links, setLinks] = useState([]); + + const [listenedLink, setListenedLink] = useState(null); + + useEffect(() => { + if (config.id && service.ready) { + setLoadingLink(true); + + service.service?.listenTo(config.id) + .then(() => WalltakerSocketService.getLink(config.id ?? 0)) + .then(link => link && setListenedLink(link)) + .finally(() => setLoadingLink(false)); + } + + return () => { + if (config.id) service.service?.muteFrom(config.id).catch(() => setListenedLink(null)); + }; + }, [config.id, service.enabled, service.ready, service.service]); + + const getLinks = () => { + setLoadingLinks(true); + + WalltakerSocketService.getLinksFromUsername(username) + .then((links) => setLinks(links ?? [])) + .catch(() => setLinks([])) + .finally(() => setLoadingLinks(false)); + }; + + const enabledAndReady = config.enabled && service.enabled && service.ready; + return ( - + { setConfig({ ...config, enabled: !service.enabled }); service.setEnabled(!service.enabled); }} type={ToggleTileType.radio} - trailing={service.enabled && !service.ready ? : } + trailing={service.enabled && !service.ready ? : + } > Use Walltaker

Let others choose wallpapers for your session, live!

- {service.enabled && service.ready && ( + {service.enabled && service.ready && !loadingLink && !listenedLink && ( <> Ready! )} + {service.enabled && service.ready && !loadingLink && listenedLink && ( + <> + + Listening to {listenedLink.id} + Change Wallpaper + + )} {service.enabled && !service.ready && ( <> Connecting... )} + {service.enabled && service.ready && loadingLink && ( + <> + + Waiting for #{config.id} + + )} {!service.enabled && ( <> @@ -54,6 +113,53 @@ export const WalltalkerSettings = () => { )}
+ + {enabledAndReady && ( + + + Username + + + + + {links.map(link => { + return ( + setConfig({ ...config, id: link.id })} + type={ToggleTileType.radio} + > + #{link.id} +

{link.terms}

+
+ ); + })} +
+ )}
); }; diff --git a/src/utils/porn-socket/walltaker.tsx b/src/utils/porn-socket/walltaker.tsx index 16fd2293..7dcb3137 100644 --- a/src/utils/porn-socket/walltaker.tsx +++ b/src/utils/porn-socket/walltaker.tsx @@ -1,13 +1,38 @@ import { PropsWithChildren, useRef } from 'react'; import { PornSocketContext, PornSocketService } from './porn-socket-service.tsx'; -class WalltakerSocketService implements PornSocketService { +export interface LinkResponse { + id: number; + expires: string; + terms: string; + blacklist: string; + post_url: string; + post_thumbnail_url: string; + post_description: string; + created_at: string; + updated_at: string; + response_type?: any; + response_text?: any; + username: string; + online: boolean; +} + +export interface UserResponse { + username: string; + id: number; + set_count: number; + online: boolean; + authenticated: boolean; + links: LinkResponse[]; +} + +export class WalltakerSocketService implements PornSocketService { private socket: WebSocket | null = null; connect(): Promise { return new Promise((res, rej) => { if (this.socket) { - switch(this.socket.readyState) { + switch (this.socket.readyState) { case this.socket.OPEN: res(); return; @@ -18,7 +43,7 @@ class WalltakerSocketService implements PornSocketService { case this.socket.CLOSING: case this.socket.CLOSED: this.socket = null; - return this.connect() + return this.connect(); } } @@ -70,6 +95,17 @@ class WalltakerSocketService implements PornSocketService { }); } + static async getLink(id: number) { + return fetch(`https://walltaker.joi.how/api/links/${id}.json`) + .then(res => res.json() as unknown as LinkResponse); + } + + static async getLinksFromUsername(username: string) { + const result = await fetch(`https://walltaker.joi.how/api/users/${username}.json`) + .then(res => res.json() as unknown as UserResponse); + return result.links; + } + private channelIdentifierFor(id: number) { return JSON.stringify({ channel: 'LinkChannel', id }); } From 184d90f9ad47b980b7f2d6d0a3cf6baf8e220516 Mon Sep 17 00:00:00 2001 From: Pup Gray Date: Sun, 19 May 2024 16:45:37 -0400 Subject: [PATCH 05/14] set up syncing when wallpaper changes --- src/settings/components/ImageSettings.tsx | 8 +++- .../components/WalltalkerSettings.tsx | 24 +++++++++--- src/utils/porn-socket/walltaker.tsx | 37 +++++++++++++++++-- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/settings/components/ImageSettings.tsx b/src/settings/components/ImageSettings.tsx index e83bf9fe..ffbaf73d 100644 --- a/src/settings/components/ImageSettings.tsx +++ b/src/settings/components/ImageSettings.tsx @@ -38,6 +38,7 @@ const SmallActionButton = styled(IconButton)` export const ImageSettings = () => { const [images, setImages] = useImages(); const { removeImage } = useLocalImages(); + const [walltakerConfig] = useSetting('walltaker'); const [selected, setSelected] = useState([]); const [clicked, setClicked] = useState(undefined); const [videoSound] = useSetting('videoSound'); @@ -45,7 +46,12 @@ export const ImageSettings = () => { return ( -

{`You have loaded ${images.length} images`}

+

+ {`You have loaded ${images.length} images`} + {walltakerConfig.enabled && + walltakerConfig.id && + ` and additional wallpapers will load from walltaker link ${walltakerConfig.id}`} +

{ + const [images, setImages] = useImages(); const [config, setConfig] = useSetting('walltaker'); const service = usePornSocketService(config.enabled); @@ -45,6 +43,20 @@ export const WalltalkerSettings = () => { service.service?.listenTo(config.id) .then(() => WalltakerSocketService.getLink(config.id ?? 0)) + .then(link => { + if (images.find(image => image.full === link.post_url)) return link + + setImages([...images, { + thumbnail: link.post_thumbnail_url, + preview: link.post_thumbnail_url, + full: link.post_url, + type: ImageType.image, + source: link.post_url, + service: ImageServiceType.e621, + id: link.post_url, + }]); + return link; + }) .then(link => link && setListenedLink(link)) .finally(() => setLoadingLink(false)); } diff --git a/src/utils/porn-socket/walltaker.tsx b/src/utils/porn-socket/walltaker.tsx index 7dcb3137..b8d6315c 100644 --- a/src/utils/porn-socket/walltaker.tsx +++ b/src/utils/porn-socket/walltaker.tsx @@ -1,5 +1,7 @@ -import { PropsWithChildren, useRef } from 'react'; +import { PropsWithChildren, useEffect, useRef } from 'react'; import { PornSocketContext, PornSocketService } from './porn-socket-service.tsx'; +import { useImages } from '../../settings'; +import { ImageServiceType, ImageType } from '../../types'; export interface LinkResponse { id: number; @@ -28,6 +30,8 @@ export interface UserResponse { export class WalltakerSocketService implements PornSocketService { private socket: WebSocket | null = null; + onChange: (link: LinkResponse) => void = () => { + }; connect(): Promise { return new Promise((res, rej) => { @@ -50,8 +54,10 @@ export class WalltakerSocketService implements PornSocketService { const socket = new WebSocket('wss://walltaker.joi.how/cable'); this.socket = socket; - socket.addEventListener('open', () => res()); - socket.addEventListener('error', () => rej()); + this.socket.addEventListener('message', this.onAnyMessage.bind(this)); + + this.socket.addEventListener('open', () => res()); + this.socket.addEventListener('error', () => rej()); }); } @@ -106,6 +112,13 @@ export class WalltakerSocketService implements PornSocketService { return result.links; } + private onAnyMessage(message: MessageEvent) { + const content = JSON.parse(message.data); + if (content.message?.success === true && content.message?.post_url) { + this.onChange(content.message as unknown as LinkResponse); + } + } + private channelIdentifierFor(id: number) { return JSON.stringify({ channel: 'LinkChannel', id }); } @@ -133,6 +146,24 @@ export class WalltakerSocketService implements PornSocketService { } export function WalltakerSocketServiceProvider({ children }: PropsWithChildren) { + const [images, setImages] = useImages(); const walltakerSocketService = useRef(new WalltakerSocketService()); + + useEffect(() => { + walltakerSocketService.current.onChange = (link) => { + if (images.find(image => image.full === link.post_url)) return; + + setImages([...images, { + thumbnail: link.post_thumbnail_url, + preview: link.post_thumbnail_url, + full: link.post_url, + type: ImageType.image, + source: link.post_url, + service: ImageServiceType.e621, + id: link.post_url, + }]); + }; + }, [images, setImages]); + return {children}; } \ No newline at end of file From 7045d881e04a6b94cd33ece63188ce180027c978 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 22 Sep 2024 20:28:04 +0200 Subject: [PATCH 06/14] applied format --- src/app/App.tsx | 4 +- src/settings/SettingsProvider.tsx | 12 ++- .../components/WalltalkerSettings.tsx | 93 ++++++++++++------- src/utils/porn-socket/porn-socket-service.tsx | 8 +- src/utils/porn-socket/walltaker.tsx | 76 ++++++++++----- 5 files changed, 129 insertions(+), 64 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 561af047..1835f0df 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -8,8 +8,8 @@ export const App = () => { - } /> - } /> + } /> + } /> diff --git a/src/settings/SettingsProvider.tsx b/src/settings/SettingsProvider.tsx index b48bf24e..aac65366 100644 --- a/src/settings/SettingsProvider.tsx +++ b/src/settings/SettingsProvider.tsx @@ -1,5 +1,11 @@ import { useCallback } from 'react'; -import { GameEvent, GameHypnoType, PlayerBody, PlayerGender, WalltakerConfig } from '../types'; +import { + GameEvent, + GameHypnoType, + PlayerBody, + PlayerGender, + WalltakerConfig, +} from '../types'; import { createLocalStorageProvider, VibrationMode } from '../utils'; import { interpolateWith } from '../utils/translate'; @@ -40,8 +46,8 @@ export const defaultSettings: Settings = { vibrations: VibrationMode.thump, walltaker: { enabled: false, - id: null - } + id: null, + }, }; const settingsStorageKey = 'settings'; diff --git a/src/settings/components/WalltalkerSettings.tsx b/src/settings/components/WalltalkerSettings.tsx index 5fed303e..a7143f8e 100644 --- a/src/settings/components/WalltalkerSettings.tsx +++ b/src/settings/components/WalltalkerSettings.tsx @@ -1,28 +1,44 @@ -import { Button, SettingsLabel, SettingsTile, Spinner, TextInput, ToggleTile, ToggleTileType } from '../../common'; -import { faCheckSquare, faPowerOff, faSpinner, faSquare } from '@fortawesome/free-solid-svg-icons'; +import { + Button, + SettingsLabel, + SettingsTile, + Spinner, + TextInput, + ToggleTile, + ToggleTileType, +} from '../../common'; +import { + faCheckSquare, + faPowerOff, + faSpinner, + faSquare, +} from '@fortawesome/free-solid-svg-icons'; import { usePornSocketService } from '../../utils/porn-socket/porn-socket-service.tsx'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import styled from 'styled-components'; import { useSetting } from '../SettingsProvider.tsx'; import { useEffect, useState } from 'react'; -import { LinkResponse, WalltakerSocketService } from '../../utils/porn-socket/walltaker.tsx'; +import { + LinkResponse, + WalltakerSocketService, +} from '../../utils/porn-socket/walltaker.tsx'; import { defaultTransition } from '../../utils'; import { motion } from 'framer-motion'; import { useImages } from '../ImageProvider.tsx'; import { ImageServiceType, ImageType } from '../../types'; const Header = styled.div` - display: flex; - gap: 1ex; - align-items: center; - margin-bottom: 0.75rem; + display: flex; + gap: 1ex; + align-items: center; + margin-bottom: 0.75rem; `; const StyledLinksForm = styled.div` - grid-column: 1 / -1; - display: grid; - grid-template-columns: auto 1fr auto; - gap: 0.75rem; + grid-column: 1 / -1; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.75rem; `; export const WalltalkerSettings = () => { @@ -41,20 +57,24 @@ export const WalltalkerSettings = () => { if (config.id && service.ready) { setLoadingLink(true); - service.service?.listenTo(config.id) + service.service + ?.listenTo(config.id) .then(() => WalltakerSocketService.getLink(config.id ?? 0)) .then(link => { - if (images.find(image => image.full === link.post_url)) return link + if (images.find(image => image.full === link.post_url)) return link; - setImages([...images, { - thumbnail: link.post_thumbnail_url, - preview: link.post_thumbnail_url, - full: link.post_url, - type: ImageType.image, - source: link.post_url, - service: ImageServiceType.e621, - id: link.post_url, - }]); + setImages([ + ...images, + { + thumbnail: link.post_thumbnail_url, + preview: link.post_thumbnail_url, + full: link.post_url, + type: ImageType.image, + source: link.post_url, + service: ImageServiceType.e621, + id: link.post_url, + }, + ]); return link; }) .then(link => link && setListenedLink(link)) @@ -62,7 +82,8 @@ export const WalltalkerSettings = () => { } return () => { - if (config.id) service.service?.muteFrom(config.id).catch(() => setListenedLink(null)); + if (config.id) + service.service?.muteFrom(config.id).catch(() => setListenedLink(null)); }; }, [config.id, service.enabled, service.ready, service.service]); @@ -70,7 +91,7 @@ export const WalltalkerSettings = () => { setLoadingLinks(true); WalltakerSocketService.getLinksFromUsername(username) - .then((links) => setLinks(links ?? [])) + .then(links => setLinks(links ?? [])) .catch(() => setLinks([])) .finally(() => setLoadingLinks(false)); }; @@ -78,7 +99,7 @@ export const WalltalkerSettings = () => { const enabledAndReady = config.enabled && service.enabled && service.ready; return ( - + { @@ -86,8 +107,13 @@ export const WalltalkerSettings = () => { service.setEnabled(!service.enabled); }} type={ToggleTileType.radio} - trailing={service.enabled && !service.ready ? : - } + trailing={ + service.enabled && !service.ready ? ( + + ) : ( + + ) + } > Use Walltaker

Let others choose wallpapers for your session, live!

@@ -103,7 +129,12 @@ export const WalltalkerSettings = () => { <> Listening to {listenedLink.id} - Change Wallpaper + + Change Wallpaper + )} {service.enabled && !service.ready && ( @@ -135,13 +166,13 @@ export const WalltalkerSettings = () => { transition={defaultTransition} > - Username + Username diff --git a/src/utils/porn-socket/porn-socket-service.tsx b/src/utils/porn-socket/porn-socket-service.tsx index 8f90cc09..fe4e17f9 100644 --- a/src/utils/porn-socket/porn-socket-service.tsx +++ b/src/utils/porn-socket/porn-socket-service.tsx @@ -21,9 +21,11 @@ export function usePornSocketService(enableDefault = false) { useEffect(() => { if (enabled) { - service?.connect() + service + ?.connect() .then(() => setReady(true)) - .catch(() => setReady(false));} + .catch(() => setReady(false)); + } return () => { service?.disconnect().then(() => setReady(false)); @@ -36,4 +38,4 @@ export function usePornSocketService(enableDefault = false) { ready, service, }; -} \ No newline at end of file +} diff --git a/src/utils/porn-socket/walltaker.tsx b/src/utils/porn-socket/walltaker.tsx index b8d6315c..0661ede0 100644 --- a/src/utils/porn-socket/walltaker.tsx +++ b/src/utils/porn-socket/walltaker.tsx @@ -1,5 +1,8 @@ import { PropsWithChildren, useEffect, useRef } from 'react'; -import { PornSocketContext, PornSocketService } from './porn-socket-service.tsx'; +import { + PornSocketContext, + PornSocketService, +} from './porn-socket-service.tsx'; import { useImages } from '../../settings'; import { ImageServiceType, ImageType } from '../../types'; @@ -30,8 +33,7 @@ export interface UserResponse { export class WalltakerSocketService implements PornSocketService { private socket: WebSocket | null = null; - onChange: (link: LinkResponse) => void = () => { - }; + onChange: (link: LinkResponse) => void = () => {}; connect(): Promise { return new Promise((res, rej) => { @@ -62,7 +64,7 @@ export class WalltakerSocketService implements PornSocketService { } disconnect(): Promise { - return new Promise((res) => { + return new Promise(res => { if (this.socket) { this.socket.addEventListener('close', () => res()); this.socket.close(); @@ -81,7 +83,10 @@ export class WalltakerSocketService implements PornSocketService { const channelId = this.channelIdentifierFor(id); const message = { command: 'subscribe', identifier: channelId }; - this.socket.addEventListener('message', this.firstMessageHandlerFor('confirm_subscription', id, rej, res)); + this.socket.addEventListener( + 'message', + this.firstMessageHandlerFor('confirm_subscription', id, rej, res) + ); this.socket.send(JSON.stringify(message)); }); } @@ -102,13 +107,15 @@ export class WalltakerSocketService implements PornSocketService { } static async getLink(id: number) { - return fetch(`https://walltaker.joi.how/api/links/${id}.json`) - .then(res => res.json() as unknown as LinkResponse); + return fetch(`https://walltaker.joi.how/api/links/${id}.json`).then( + res => res.json() as unknown as LinkResponse + ); } static async getLinksFromUsername(username: string) { - const result = await fetch(`https://walltaker.joi.how/api/users/${username}.json`) - .then(res => res.json() as unknown as UserResponse); + const result = await fetch( + `https://walltaker.joi.how/api/users/${username}.json` + ).then(res => res.json() as unknown as UserResponse); return result.links; } @@ -123,13 +130,21 @@ export class WalltakerSocketService implements PornSocketService { return JSON.stringify({ channel: 'LinkChannel', id }); } - private firstMessageHandlerFor(eventName: string, id: number, rej: (message?: string) => void, res: () => void) { + private firstMessageHandlerFor( + eventName: string, + id: number, + rej: (message?: string) => void, + res: () => void + ) { const MAX_RETRIES = 4; let tries = 0; const handler = (message: MessageEvent) => { const content = JSON.parse(message.data); - if (content.type === eventName && content.identifier === this.channelIdentifierFor(id)) { + if ( + content.type === eventName && + content.identifier === this.channelIdentifierFor(id) + ) { this.socket?.removeEventListener('message', handler); res(); return; @@ -137,7 +152,9 @@ export class WalltakerSocketService implements PornSocketService { if (++tries > MAX_RETRIES) { this.socket?.removeEventListener('message', handler); - rej(`Max retries hit when waiting for subscription confirmation for Link#${id}.`); + rej( + `Max retries hit when waiting for subscription confirmation for Link#${id}.` + ); } }; @@ -145,25 +162,34 @@ export class WalltakerSocketService implements PornSocketService { } } -export function WalltakerSocketServiceProvider({ children }: PropsWithChildren) { +export function WalltakerSocketServiceProvider({ + children, +}: PropsWithChildren) { const [images, setImages] = useImages(); const walltakerSocketService = useRef(new WalltakerSocketService()); useEffect(() => { - walltakerSocketService.current.onChange = (link) => { + walltakerSocketService.current.onChange = link => { if (images.find(image => image.full === link.post_url)) return; - setImages([...images, { - thumbnail: link.post_thumbnail_url, - preview: link.post_thumbnail_url, - full: link.post_url, - type: ImageType.image, - source: link.post_url, - service: ImageServiceType.e621, - id: link.post_url, - }]); + setImages([ + ...images, + { + thumbnail: link.post_thumbnail_url, + preview: link.post_thumbnail_url, + full: link.post_url, + type: ImageType.image, + source: link.post_url, + service: ImageServiceType.e621, + id: link.post_url, + }, + ]); }; }, [images, setImages]); - return {children}; -} \ No newline at end of file + return ( + + {children} + + ); +} From 35cd20b15e1cb2539e210df38f5e360de6afa0d7 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 22 Sep 2024 20:45:10 +0200 Subject: [PATCH 07/14] moved walltalker into service tabs --- src/game/components/GameSettings.tsx | 2 - src/settings/SettingsSection.tsx | 2 - src/settings/components/ServiceSettings.tsx | 2 +- .../components/WalltalkerSettings.tsx | 208 ---------------- src/settings/components/index.ts | 1 - src/walltalker/WalltalkerSearch.tsx | 223 ++++++++++++++++-- 6 files changed, 208 insertions(+), 230 deletions(-) delete mode 100644 src/settings/components/WalltalkerSettings.tsx diff --git a/src/game/components/GameSettings.tsx b/src/game/components/GameSettings.tsx index 18c0abfb..d2967d72 100644 --- a/src/game/components/GameSettings.tsx +++ b/src/game/components/GameSettings.tsx @@ -13,7 +13,6 @@ import { PaceSettings, PlayerSettings, VibratorSettings, - WalltalkerSettings, } from '../../settings'; import { GamePhase, useGameValue, useSendMessage } from '../GameProvider'; import { useFullscreen, useLooping } from '../../utils'; @@ -60,7 +59,6 @@ const GameSettingsDialog: React.FC = props => { - } diff --git a/src/settings/SettingsSection.tsx b/src/settings/SettingsSection.tsx index 0ac3c508..7193ce53 100644 --- a/src/settings/SettingsSection.tsx +++ b/src/settings/SettingsSection.tsx @@ -12,7 +12,6 @@ import { BoardSettings, VibratorSettings, TradeSettings, - WalltalkerSettings, } from './components'; const StyledSettingsSection = styled(ContentSection)` @@ -33,7 +32,6 @@ export const SettingsSection = () => { - ); diff --git a/src/settings/components/ServiceSettings.tsx b/src/settings/components/ServiceSettings.tsx index bc66e642..a372c383 100644 --- a/src/settings/components/ServiceSettings.tsx +++ b/src/settings/components/ServiceSettings.tsx @@ -29,7 +29,7 @@ export const ServiceSettings = () => { { - const [images, setImages] = useImages(); - const [config, setConfig] = useSetting('walltaker'); - const service = usePornSocketService(config.enabled); - - const [username, setUsername] = useState(''); - const [loadingLinks, setLoadingLinks] = useState(false); - const [loadingLink, setLoadingLink] = useState(false); - const [links, setLinks] = useState([]); - - const [listenedLink, setListenedLink] = useState(null); - - useEffect(() => { - if (config.id && service.ready) { - setLoadingLink(true); - - service.service - ?.listenTo(config.id) - .then(() => WalltakerSocketService.getLink(config.id ?? 0)) - .then(link => { - if (images.find(image => image.full === link.post_url)) return link; - - setImages([ - ...images, - { - thumbnail: link.post_thumbnail_url, - preview: link.post_thumbnail_url, - full: link.post_url, - type: ImageType.image, - source: link.post_url, - service: ImageServiceType.e621, - id: link.post_url, - }, - ]); - return link; - }) - .then(link => link && setListenedLink(link)) - .finally(() => setLoadingLink(false)); - } - - return () => { - if (config.id) - service.service?.muteFrom(config.id).catch(() => setListenedLink(null)); - }; - }, [config.id, service.enabled, service.ready, service.service]); - - const getLinks = () => { - setLoadingLinks(true); - - WalltakerSocketService.getLinksFromUsername(username) - .then(links => setLinks(links ?? [])) - .catch(() => setLinks([])) - .finally(() => setLoadingLinks(false)); - }; - - const enabledAndReady = config.enabled && service.enabled && service.ready; - - return ( - - { - setConfig({ ...config, enabled: !service.enabled }); - service.setEnabled(!service.enabled); - }} - type={ToggleTileType.radio} - trailing={ - service.enabled && !service.ready ? ( - - ) : ( - - ) - } - > - Use Walltaker -

Let others choose wallpapers for your session, live!

-
-
- {service.enabled && service.ready && !loadingLink && !listenedLink && ( - <> - - Ready! - - )} - {service.enabled && service.ready && !loadingLink && listenedLink && ( - <> - - Listening to {listenedLink.id} - - Change Wallpaper - - - )} - {service.enabled && !service.ready && ( - <> - - Connecting... - - )} - {service.enabled && service.ready && loadingLink && ( - <> - - Waiting for #{config.id} - - )} - {!service.enabled && ( - <> - - Disabled - - )} -
- - {enabledAndReady && ( - - - Username - - - - - {links.map(link => { - return ( - setConfig({ ...config, id: link.id })} - type={ToggleTileType.radio} - > - #{link.id} -

{link.terms}

-
- ); - })} -
- )} -
- ); -}; diff --git a/src/settings/components/index.ts b/src/settings/components/index.ts index d4d4c22a..c20b5673 100644 --- a/src/settings/components/index.ts +++ b/src/settings/components/index.ts @@ -9,4 +9,3 @@ export * from './PlayerSettings'; export * from './ServiceSettings'; export * from './TradeSettings'; export * from './VibratorSettings'; -export * from './WalltalkerSettings'; diff --git a/src/walltalker/WalltalkerSearch.tsx b/src/walltalker/WalltalkerSearch.tsx index da0d26f3..8c153c4b 100644 --- a/src/walltalker/WalltalkerSearch.tsx +++ b/src/walltalker/WalltalkerSearch.tsx @@ -1,22 +1,213 @@ +import { + faSpinner, + faPowerOff, + faCheckSquare, + faSquare, +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faPersonDigging } from '@fortawesome/free-solid-svg-icons'; +import { motion } from 'framer-motion'; +import { useState, useEffect } from 'react'; +import { + ToggleTile, + ToggleTileType, + SettingsLabel, + TextInput, + Button, + Spinner, +} from '../common'; +import { useImages, useSetting } from '../settings'; +import { ImageType, ImageServiceType } from '../types'; +import { defaultTransition } from '../utils'; +import { usePornSocketService } from '../utils/porn-socket/porn-socket-service'; +import { + LinkResponse, + WalltakerSocketService, +} from '../utils/porn-socket/walltaker'; +import styled from 'styled-components'; + +const Header = styled.div` + display: flex; + gap: 1ex; + align-items: center; + margin-bottom: 0.75rem; +`; + +const StyledLinksForm = styled.div` + grid-column: 1 / -1; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.75rem; +`; export const WalltalkerSearch = () => { + const [images, setImages] = useImages(); + const [config, setConfig] = useSetting('walltaker'); + const service = usePornSocketService(config.enabled); + + const [username, setUsername] = useState(''); + const [loadingLinks, setLoadingLinks] = useState(false); + const [loadingLink, setLoadingLink] = useState(false); + const [links, setLinks] = useState([]); + + const [listenedLink, setListenedLink] = useState(null); + + useEffect(() => { + if (config.id && service.ready) { + setLoadingLink(true); + + service.service + ?.listenTo(config.id) + .then(() => WalltakerSocketService.getLink(config.id ?? 0)) + .then(link => { + if (images.find(image => image.full === link.post_url)) return link; + + setImages([ + ...images, + { + thumbnail: link.post_thumbnail_url, + preview: link.post_thumbnail_url, + full: link.post_url, + type: ImageType.image, + source: link.post_url, + service: ImageServiceType.e621, + id: link.post_url, + }, + ]); + return link; + }) + .then(link => link && setListenedLink(link)) + .finally(() => setLoadingLink(false)); + } + + return () => { + if (config.id) + service.service?.muteFrom(config.id).catch(() => setListenedLink(null)); + }; + }, [ + config.id, + images, + service.enabled, + service.ready, + service.service, + setImages, + ]); + + const getLinks = () => { + setLoadingLinks(true); + + WalltakerSocketService.getLinksFromUsername(username) + .then(links => setLinks(links ?? [])) + .catch(() => setLinks([])) + .finally(() => setLoadingLinks(false)); + }; + + const enabledAndReady = config.enabled && service.enabled && service.ready; + return ( -
-

- -

-

There is nothing here yet :3

-
+ <> + { + setConfig({ ...config, enabled: !service.enabled }); + service.setEnabled(!service.enabled); + }} + type={ToggleTileType.radio} + trailing={ + service.enabled && !service.ready ? ( + + ) : ( + + ) + } + > + Use Walltaker +

Let others choose wallpapers for your session, live!

+
+
+ {service.enabled && service.ready && !loadingLink && !listenedLink && ( + <> + + Ready! + + )} + {service.enabled && service.ready && !loadingLink && listenedLink && ( + <> + + Listening to {listenedLink.id} + + Change Wallpaper + + + )} + {service.enabled && !service.ready && ( + <> + + Connecting... + + )} + {service.enabled && service.ready && loadingLink && ( + <> + + Waiting for #{config.id} + + )} + {!service.enabled && ( + <> + + Disabled + + )} +
+ + {enabledAndReady && ( + + + Username + + + + + {links.map(link => { + return ( + setConfig({ ...config, id: link.id })} + type={ToggleTileType.radio} + > + #{link.id} +

{link.terms}

+
+ ); + })} +
+ )} + ); }; From 39da4f6897e50dc2c3cce0e198ea9298e46c85e7 Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 22 Sep 2024 20:47:47 +0200 Subject: [PATCH 08/14] moved walltalker provider --- src/app/App.tsx | 15 ++++++--------- src/index.tsx | 13 ++++++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 1835f0df..cf2fd360 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,17 +1,14 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { HomePage } from '../home'; import { GamePage } from '../game'; -import { WalltakerSocketServiceProvider } from '../utils/porn-socket/walltaker.tsx'; export const App = () => { return ( - - - - } /> - } /> - - - + + + } /> + } /> + + ); }; diff --git a/src/index.tsx b/src/index.tsx index 6273059e..3a53c752 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,17 +6,20 @@ import { SettingsProvider, ImageProvider } from './settings'; import { E621Provider } from './e621'; import { VibratorProvider } from './utils'; import { LocalImageProvider } from './local/LocalProvider.tsx'; +import { WalltakerSocketServiceProvider } from './utils/porn-socket/walltaker.tsx'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - + + + + + + + From 2ff19f64684780b1f708c8ffc40967ff9f250a78 Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 23 Sep 2024 05:36:22 +0200 Subject: [PATCH 09/14] updated walltalker service (WIP) --- src/index.tsx | 8 +- src/settings/SettingsProvider.tsx | 13 +- src/utils/porn-socket/porn-socket-service.tsx | 41 ---- src/utils/porn-socket/walltaker.tsx | 195 ------------------ src/utils/state.tsx | 4 +- src/walltalker/WalltalkerProvider.tsx | 139 +++++++++++++ src/walltalker/WalltalkerSearch.tsx | 194 +++++------------ src/walltalker/WalltalkerService.ts | 183 ++++++++++++++++ src/walltalker/index.ts | 2 + 9 files changed, 389 insertions(+), 390 deletions(-) delete mode 100644 src/utils/porn-socket/porn-socket-service.tsx delete mode 100644 src/utils/porn-socket/walltaker.tsx create mode 100644 src/walltalker/WalltalkerProvider.tsx create mode 100644 src/walltalker/WalltalkerService.ts diff --git a/src/index.tsx b/src/index.tsx index 3a53c752..91830a31 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,21 +5,21 @@ import './index.css'; import { SettingsProvider, ImageProvider } from './settings'; import { E621Provider } from './e621'; import { VibratorProvider } from './utils'; -import { LocalImageProvider } from './local/LocalProvider.tsx'; -import { WalltakerSocketServiceProvider } from './utils/porn-socket/walltaker.tsx'; +import { LocalImageProvider } from './local'; +import { WalltalkerProvider } from './walltalker'; ReactDOM.createRoot(document.getElementById('root')!).render( - + - + diff --git a/src/settings/SettingsProvider.tsx b/src/settings/SettingsProvider.tsx index aac65366..22334c27 100644 --- a/src/settings/SettingsProvider.tsx +++ b/src/settings/SettingsProvider.tsx @@ -1,11 +1,5 @@ import { useCallback } from 'react'; -import { - GameEvent, - GameHypnoType, - PlayerBody, - PlayerGender, - WalltakerConfig, -} from '../types'; +import { GameEvent, GameHypnoType, PlayerBody, PlayerGender } from '../types'; import { createLocalStorageProvider, VibrationMode } from '../utils'; import { interpolateWith } from '../utils/translate'; @@ -25,7 +19,6 @@ export interface Settings { highRes: boolean; videoSound: boolean; vibrations: VibrationMode; - walltaker: WalltakerConfig; } export const defaultSettings: Settings = { @@ -44,10 +37,6 @@ export const defaultSettings: Settings = { highRes: false, videoSound: false, vibrations: VibrationMode.thump, - walltaker: { - enabled: false, - id: null, - }, }; const settingsStorageKey = 'settings'; diff --git a/src/utils/porn-socket/porn-socket-service.tsx b/src/utils/porn-socket/porn-socket-service.tsx deleted file mode 100644 index fe4e17f9..00000000 --- a/src/utils/porn-socket/porn-socket-service.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { createContext, useContext } from 'use-context-selector'; -import { useEffect, useState } from 'react'; - -export interface PornSocketService { - connect(): Promise; - - disconnect(): Promise; - - listenTo(id: number): Promise; - - muteFrom(id: number): Promise; -} - -export const PornSocketContext = createContext(null); - -export function usePornSocketService(enableDefault = false) { - const service = useContext(PornSocketContext); - - const [enabled, setEnabled] = useState(enableDefault); - const [ready, setReady] = useState(false); - - useEffect(() => { - if (enabled) { - service - ?.connect() - .then(() => setReady(true)) - .catch(() => setReady(false)); - } - - return () => { - service?.disconnect().then(() => setReady(false)); - }; - }, [service, enabled]); - - return { - enabled: Boolean(service) && enabled, - setEnabled, - ready, - service, - }; -} diff --git a/src/utils/porn-socket/walltaker.tsx b/src/utils/porn-socket/walltaker.tsx deleted file mode 100644 index 0661ede0..00000000 --- a/src/utils/porn-socket/walltaker.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { PropsWithChildren, useEffect, useRef } from 'react'; -import { - PornSocketContext, - PornSocketService, -} from './porn-socket-service.tsx'; -import { useImages } from '../../settings'; -import { ImageServiceType, ImageType } from '../../types'; - -export interface LinkResponse { - id: number; - expires: string; - terms: string; - blacklist: string; - post_url: string; - post_thumbnail_url: string; - post_description: string; - created_at: string; - updated_at: string; - response_type?: any; - response_text?: any; - username: string; - online: boolean; -} - -export interface UserResponse { - username: string; - id: number; - set_count: number; - online: boolean; - authenticated: boolean; - links: LinkResponse[]; -} - -export class WalltakerSocketService implements PornSocketService { - private socket: WebSocket | null = null; - onChange: (link: LinkResponse) => void = () => {}; - - connect(): Promise { - return new Promise((res, rej) => { - if (this.socket) { - switch (this.socket.readyState) { - case this.socket.OPEN: - res(); - return; - case this.socket.CONNECTING: - this.socket.addEventListener('open', () => res()); - this.socket.addEventListener('error', () => rej()); - return; - case this.socket.CLOSING: - case this.socket.CLOSED: - this.socket = null; - return this.connect(); - } - } - - const socket = new WebSocket('wss://walltaker.joi.how/cable'); - this.socket = socket; - - this.socket.addEventListener('message', this.onAnyMessage.bind(this)); - - this.socket.addEventListener('open', () => res()); - this.socket.addEventListener('error', () => rej()); - }); - } - - disconnect(): Promise { - return new Promise(res => { - if (this.socket) { - this.socket.addEventListener('close', () => res()); - this.socket.close(); - } - this.socket = null; - }); - } - - listenTo(id: number): Promise { - return new Promise((res, rej) => { - if (!this.socket) { - rej('No socket connected.'); - return; - } - - const channelId = this.channelIdentifierFor(id); - const message = { command: 'subscribe', identifier: channelId }; - - this.socket.addEventListener( - 'message', - this.firstMessageHandlerFor('confirm_subscription', id, rej, res) - ); - this.socket.send(JSON.stringify(message)); - }); - } - - muteFrom(id: number): Promise { - return new Promise((res, rej) => { - if (!this.socket) { - rej('No socket connected.'); - return; - } - - const channelId = this.channelIdentifierFor(id); - const message = { command: 'unsubscribe', identifier: channelId }; - - this.socket.send(JSON.stringify(message)); - res(); - }); - } - - static async getLink(id: number) { - return fetch(`https://walltaker.joi.how/api/links/${id}.json`).then( - res => res.json() as unknown as LinkResponse - ); - } - - static async getLinksFromUsername(username: string) { - const result = await fetch( - `https://walltaker.joi.how/api/users/${username}.json` - ).then(res => res.json() as unknown as UserResponse); - return result.links; - } - - private onAnyMessage(message: MessageEvent) { - const content = JSON.parse(message.data); - if (content.message?.success === true && content.message?.post_url) { - this.onChange(content.message as unknown as LinkResponse); - } - } - - private channelIdentifierFor(id: number) { - return JSON.stringify({ channel: 'LinkChannel', id }); - } - - private firstMessageHandlerFor( - eventName: string, - id: number, - rej: (message?: string) => void, - res: () => void - ) { - const MAX_RETRIES = 4; - let tries = 0; - - const handler = (message: MessageEvent) => { - const content = JSON.parse(message.data); - if ( - content.type === eventName && - content.identifier === this.channelIdentifierFor(id) - ) { - this.socket?.removeEventListener('message', handler); - res(); - return; - } - - if (++tries > MAX_RETRIES) { - this.socket?.removeEventListener('message', handler); - rej( - `Max retries hit when waiting for subscription confirmation for Link#${id}.` - ); - } - }; - - return handler; - } -} - -export function WalltakerSocketServiceProvider({ - children, -}: PropsWithChildren) { - const [images, setImages] = useImages(); - const walltakerSocketService = useRef(new WalltakerSocketService()); - - useEffect(() => { - walltakerSocketService.current.onChange = link => { - if (images.find(image => image.full === link.post_url)) return; - - setImages([ - ...images, - { - thumbnail: link.post_thumbnail_url, - preview: link.post_thumbnail_url, - full: link.post_url, - type: ImageType.image, - source: link.post_url, - service: ImageServiceType.e621, - id: link.post_url, - }, - ]); - }; - }, [images, setImages]); - - return ( - - {children} - - ); -} diff --git a/src/utils/state.tsx b/src/utils/state.tsx index 6b643a02..d5a85270 100644 --- a/src/utils/state.tsx +++ b/src/utils/state.tsx @@ -14,6 +14,8 @@ interface StateContextType { setData: React.Dispatch>; } +export type StateAndSetter = [T, React.Dispatch>]; + export function createStateProvider({ defaultData: globalDefaultData, }: StateProviderOptions) { @@ -48,7 +50,7 @@ export function createStateProvider({ ); }; - const useProvider = (): [T, React.Dispatch>] => { + const useProvider = (): StateAndSetter => { const context = useContext(StateContext); if (!context) { throw new Error('useProvider must be used within its Provider'); diff --git a/src/walltalker/WalltalkerProvider.tsx b/src/walltalker/WalltalkerProvider.tsx new file mode 100644 index 00000000..2bfefd9e --- /dev/null +++ b/src/walltalker/WalltalkerProvider.tsx @@ -0,0 +1,139 @@ +/* eslint-disable react-refresh/only-export-components */ +import { + createContext, + PropsWithChildren, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { createLocalStorageProvider, StateAndSetter } from '../utils'; +import { WalltalkerLink, WalltalkerService } from './WalltalkerService'; +import { uniqBy } from 'lodash'; +import { useImages } from '../settings'; +import { ImageServiceType, ImageType } from '../types'; + +export interface WalltalkerSettings { + enabled?: boolean; + username?: string; + ids?: number[]; +} + +const walltalkerStorageKey = 'walltalker'; + +const { + Provider: WalltalkerSettingsProvider, + useProvider: useWalltalkerSettings, +} = createLocalStorageProvider({ + key: walltalkerStorageKey, + defaultData: {}, +}); + +const WalltalkerServiceProvider: React.FC = ({ + children, +}) => { + const settingsRef = useWalltalkerSettings(); + const service = useMemo(() => new WalltalkerService(), []); + const [, setImages] = useImages(); + + const [settings] = settingsRef; + + const [data, setData] = useState({}); + + const onLink = useCallback( + (link: WalltalkerLink) => { + setData(prev => ({ + ...prev, + links: uniqBy([link, ...(prev.links ?? [])], 'id'), + })); + setImages(prev => + uniqBy( + [ + { + thumbnail: link.post_thumbnail_url, + preview: link.post_thumbnail_url, + full: link.post_url, + type: ImageType.image, + source: link.post_url, + service: ImageServiceType.e621, + id: link.post_url, + }, + ...prev, + ], + 'id' + ) + ); + }, + [setImages] + ); + + useEffect(() => { + if (settings.enabled) { + service + .connect() + .then(() => { + setData(prev => ({ ...prev, connected: true })); + service.addLinkListener(onLink); + }) + .catch(() => setData({ connected: false })); + } + + return () => { + service.disconnect().then(() => { + setData(prev => ({ ...prev, connected: false })); + service.removeLinkListener(onLink); + }); + }; + }, [onLink, service, settings.enabled]); + + useEffect(() => { + if (data.connected) { + for (const id of settings.ids ?? []) { + service.listenTo(id); + } + // TODO: add past history? + } + }, [data.connected, service, settings.enabled, settings.ids]); + + return ( + + {children} + + ); +}; + +export interface WalltalkerData { + connected?: boolean; + links?: WalltalkerLink[]; +} + +export interface WalltalkerContext { + settings: StateAndSetter; + service: WalltalkerService; + data: WalltalkerData; +} + +const WalltalkerContext = createContext(null); + +export const WalltalkerProvider: React.FC = ({ + children, +}) => { + return ( + + {children} + + ); +}; + +export const useWalltalker = (): WalltalkerContext => { + const context = useContext(WalltalkerContext); + + if (!context) { + throw new Error('useWalltalker must be used within a WalltalkerProvider'); + } + + return context; +}; diff --git a/src/walltalker/WalltalkerSearch.tsx b/src/walltalker/WalltalkerSearch.tsx index 8c153c4b..18a59bb9 100644 --- a/src/walltalker/WalltalkerSearch.tsx +++ b/src/walltalker/WalltalkerSearch.tsx @@ -1,35 +1,24 @@ -import { - faSpinner, - faPowerOff, - faCheckSquare, - faSquare, -} from '@fortawesome/free-solid-svg-icons'; +import { faSpinner, faWalkieTalkie } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { motion } from 'framer-motion'; -import { useState, useEffect } from 'react'; import { ToggleTile, ToggleTileType, + SettingsDescription, SettingsLabel, TextInput, Button, Spinner, } from '../common'; -import { useImages, useSetting } from '../settings'; -import { ImageType, ImageServiceType } from '../types'; -import { defaultTransition } from '../utils'; -import { usePornSocketService } from '../utils/porn-socket/porn-socket-service'; -import { - LinkResponse, - WalltakerSocketService, -} from '../utils/porn-socket/walltaker'; import styled from 'styled-components'; +import { useWalltalker } from './WalltalkerProvider'; +import { motion } from 'framer-motion'; +import { defaultTransition } from '../utils'; +import { useCallback, useState } from 'react'; +import { WalltalkerLink } from './WalltalkerService'; -const Header = styled.div` - display: flex; - gap: 1ex; - align-items: center; - margin-bottom: 0.75rem; +const StyledWalltalkerSearch = styled.div` + display: grid; + grid-template-columns: auto 1fr auto; `; const StyledLinksForm = styled.div` @@ -40,129 +29,46 @@ const StyledLinksForm = styled.div` `; export const WalltalkerSearch = () => { - const [images, setImages] = useImages(); - const [config, setConfig] = useSetting('walltaker'); - const service = usePornSocketService(config.enabled); - - const [username, setUsername] = useState(''); - const [loadingLinks, setLoadingLinks] = useState(false); - const [loadingLink, setLoadingLink] = useState(false); - const [links, setLinks] = useState([]); - - const [listenedLink, setListenedLink] = useState(null); + const { service, settings: settingsRef, data } = useWalltalker(); - useEffect(() => { - if (config.id && service.ready) { - setLoadingLink(true); + const [settings, setSettings] = settingsRef; + const { connected } = data; - service.service - ?.listenTo(config.id) - .then(() => WalltakerSocketService.getLink(config.id ?? 0)) - .then(link => { - if (images.find(image => image.full === link.post_url)) return link; + const [searchLinks, setSearchLinks] = useState([]); + const [loading, setLoading] = useState(false); - setImages([ - ...images, - { - thumbnail: link.post_thumbnail_url, - preview: link.post_thumbnail_url, - full: link.post_url, - type: ImageType.image, - source: link.post_url, - service: ImageServiceType.e621, - id: link.post_url, - }, - ]); - return link; - }) - .then(link => link && setListenedLink(link)) - .finally(() => setLoadingLink(false)); - } - - return () => { - if (config.id) - service.service?.muteFrom(config.id).catch(() => setListenedLink(null)); - }; - }, [ - config.id, - images, - service.enabled, - service.ready, - service.service, - setImages, - ]); - - const getLinks = () => { - setLoadingLinks(true); - - WalltakerSocketService.getLinksFromUsername(username) - .then(links => setLinks(links ?? [])) - .catch(() => setLinks([])) - .finally(() => setLoadingLinks(false)); - }; - - const enabledAndReady = config.enabled && service.enabled && service.ready; + const search = useCallback(async () => { + setLoading(true); + if (!settings.username) return; + const links = await service.getLinksFromUsername(settings.username); + setSearchLinks(links); + setLoading(false); + }, [service, settings.username]); return ( - <> + + + Automatically add images from your Walltalker links! + { - setConfig({ ...config, enabled: !service.enabled }); - service.setEnabled(!service.enabled); + setSettings({ ...settings, enabled: !settings.enabled }); }} type={ToggleTileType.radio} trailing={ - service.enabled && !service.ready ? ( + settings.enabled && !connected ? ( ) : ( - + ) } > Use Walltaker

Let others choose wallpapers for your session, live!

-
- {service.enabled && service.ready && !loadingLink && !listenedLink && ( - <> - - Ready! - - )} - {service.enabled && service.ready && !loadingLink && listenedLink && ( - <> - - Listening to {listenedLink.id} - - Change Wallpaper - - - )} - {service.enabled && !service.ready && ( - <> - - Connecting... - - )} - {service.enabled && service.ready && loadingLink && ( - <> - - Waiting for #{config.id} - - )} - {!service.enabled && ( - <> - - Disabled - - )} -
- {enabledAndReady && ( + {connected && ( { Username { + setSettings({ ...settings, username: value }); + }} + onSubmit={search} placeholder='Enter walltaker username...' style={{ gridColumn: '2 / -1' }} - disabled={!enabledAndReady} + disabled={!connected} /> - - {links.map(link => { + Search results + {searchLinks.map(link => { return ( setConfig({ ...config, id: link.id })} + value={settings.ids?.includes(link.id)} + onClick={() => { + if (settings.ids?.includes(link.id)) { + setSettings({ + ...settings, + ids: settings.ids?.filter(id => id !== link.id), + }); + } else { + setSettings({ + ...settings, + ids: [...(settings.ids ?? []), link.id], + }); + } + }} type={ToggleTileType.radio} > #{link.id} @@ -208,6 +128,6 @@ export const WalltalkerSearch = () => { })} )} - +
); }; diff --git a/src/walltalker/WalltalkerService.ts b/src/walltalker/WalltalkerService.ts new file mode 100644 index 00000000..83ee4bfa --- /dev/null +++ b/src/walltalker/WalltalkerService.ts @@ -0,0 +1,183 @@ +import axios, { AxiosInstance } from 'axios'; +import { isEqual } from 'lodash'; + +export interface WalltalkerLink { + id: number; + expires: string; + terms: string; + blacklist: string; + post_url: string; + post_thumbnail_url: string; + post_description: string; + created_at: string; + updated_at: string; + response_type?: string; + response_text?: string; + username: string; + online: boolean; +} + +export interface WalltalkerUser { + username: string; + id: number; + set_count: number; + online: boolean; + authenticated: boolean; + links: WalltalkerLink[]; +} + +export class WalltalkerService { + constructor() { + this.axiosInstance = axios.create({ + baseURL: 'https://walltaker.joi.how/api', + }); + } + + private axiosInstance: AxiosInstance; + private socket: WebSocket | null = null; + + private subscriptions: number[] = []; + + get subcriptions() { + return this.subscriptions.slice(); + } + + connect(): Promise { + return new Promise((res, rej) => { + if (this.socket) { + switch (this.socket.readyState) { + case this.socket.OPEN: + res(); + return; + case this.socket.CONNECTING: + this.socket.addEventListener('open', () => res()); + this.socket.addEventListener('error', () => rej()); + return; + case this.socket.CLOSING: + case this.socket.CLOSED: + this.socket = null; + return this.connect(); + } + } + + const socket = new WebSocket('wss://walltaker.joi.how/cable'); + this.socket = socket; + + this.socket.addEventListener('message', this.onAnyMessage.bind(this)); + + this.socket.addEventListener('open', () => res()); + this.socket.addEventListener('error', () => rej()); + }); + } + + disconnect(): Promise { + return new Promise(res => { + if (this.socket) { + this.socket.addEventListener('close', () => res()); + this.socket.close(); + } + this.socket = null; + this.subscriptions = []; + }); + } + + listenTo(id: number): Promise { + return new Promise((resolve, reject) => { + const MAX_RETRIES = 4; + let tries = 0; + const eventName = 'confirm_subscription'; + + if (!this.socket) { + reject('No socket connected.'); + return; + } + + const handler = (message: MessageEvent) => { + if (!this.socket) return; + + const content = JSON.parse(message.data); + if (content.identifier) { + content.identifier = JSON.parse(content.identifier); + } + + console.log(content); + + if ( + content.type === eventName && + isEqual(content.identifier, this.channelIdentifierFor(id)) + ) { + console.log('Handshake confirmed for Link#' + id); + this.socket.removeEventListener('message', handler); + this.subscriptions.push(id); + resolve(); + return; + } + + if (++tries > MAX_RETRIES) { + this.socket.removeEventListener('message', handler); + reject( + `Max retries hit when waiting for handshake confirmation for Link#${id}.` + ); + } + }; + + this.socket.addEventListener('message', handler); + + const channelId = this.channelIdentifierFor(id); + const message = { command: 'subscribe', identifier: channelId }; + + this.socket.send(JSON.stringify(message)); + }); + } + + muteFrom(id: number): Promise { + return new Promise((res, rej) => { + if (!this.socket) { + rej('No socket connected.'); + return; + } + + const channelId = JSON.stringify(this.channelIdentifierFor(id)); + const message = { command: 'unsubscribe', identifier: channelId }; + + this.socket.send(JSON.stringify(message)); + this.subscriptions = this.subscriptions.filter(sub => sub !== id); + res(); + }); + } + + private channelIdentifierFor(id: number) { + return { channel: 'LinkChannel', id }; + } + + async getLink(id: number) { + return this.axiosInstance + .get(`/links/${id}.json`) + .then(res => res.data); + } + + async getLinksFromUsername(username: string) { + return this.axiosInstance + .get(`/users/${username}.json`) + .then(res => res.data.links); + } + + private onAnyMessage(message: MessageEvent) { + const content = JSON.parse(message.data); + if (content.message?.success === true && content.message?.post_url) { + this.listeners.forEach(listener => + listener(content.message as WalltalkerLink) + ); + } + } + + private listeners: ((link: WalltalkerLink) => void)[] = []; + + addLinkListener(callback: (link: WalltalkerLink) => void) { + this.listeners.push(callback); + } + + removeLinkListener(callback: (link: WalltalkerLink) => void) { + this.listeners = this.listeners.filter(listener => listener !== callback); + } +} diff --git a/src/walltalker/index.ts b/src/walltalker/index.ts index 18018c6e..2778046e 100644 --- a/src/walltalker/index.ts +++ b/src/walltalker/index.ts @@ -1 +1,3 @@ +export * from './WalltalkerProvider'; export * from './WalltalkerSearch'; +export * from './WalltalkerService'; From 3ef7269ecc9eccef5a49d8e81f46caa467566e4c Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 23 Sep 2024 14:35:02 +0200 Subject: [PATCH 10/14] updated link UI --- src/settings/components/ImageSettings.tsx | 8 +------- src/walltalker/WalltalkerSearch.tsx | 3 ++- src/walltalker/WalltalkerService.ts | 15 ++++++++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/settings/components/ImageSettings.tsx b/src/settings/components/ImageSettings.tsx index ffbaf73d..e83bf9fe 100644 --- a/src/settings/components/ImageSettings.tsx +++ b/src/settings/components/ImageSettings.tsx @@ -38,7 +38,6 @@ const SmallActionButton = styled(IconButton)` export const ImageSettings = () => { const [images, setImages] = useImages(); const { removeImage } = useLocalImages(); - const [walltakerConfig] = useSetting('walltaker'); const [selected, setSelected] = useState([]); const [clicked, setClicked] = useState(undefined); const [videoSound] = useSetting('videoSound'); @@ -46,12 +45,7 @@ export const ImageSettings = () => { return ( -

- {`You have loaded ${images.length} images`} - {walltakerConfig.enabled && - walltakerConfig.id && - ` and additional wallpapers will load from walltaker link ${walltakerConfig.id}`} -

+

{`You have loaded ${images.length} images`}

{ type={ToggleTileType.radio} > #{link.id} -

{link.terms}

+ {/* dangerously set inner html */} +

); })} diff --git a/src/walltalker/WalltalkerService.ts b/src/walltalker/WalltalkerService.ts index 83ee4bfa..86448384 100644 --- a/src/walltalker/WalltalkerService.ts +++ b/src/walltalker/WalltalkerService.ts @@ -100,8 +100,6 @@ export class WalltalkerService { content.identifier = JSON.parse(content.identifier); } - console.log(content); - if ( content.type === eventName && isEqual(content.identifier, this.channelIdentifierFor(id)) @@ -124,7 +122,10 @@ export class WalltalkerService { this.socket.addEventListener('message', handler); const channelId = this.channelIdentifierFor(id); - const message = { command: 'subscribe', identifier: channelId }; + const message = { + command: 'subscribe', + identifier: JSON.stringify(channelId), + }; this.socket.send(JSON.stringify(message)); }); @@ -137,8 +138,11 @@ export class WalltalkerService { return; } - const channelId = JSON.stringify(this.channelIdentifierFor(id)); - const message = { command: 'unsubscribe', identifier: channelId }; + const channelId = this.channelIdentifierFor(id); + const message = { + command: 'unsubscribe', + identifier: JSON.stringify(channelId), + }; this.socket.send(JSON.stringify(message)); this.subscriptions = this.subscriptions.filter(sub => sub !== id); @@ -169,6 +173,7 @@ export class WalltalkerService { listener(content.message as WalltalkerLink) ); } + console.log(content); } private listeners: ((link: WalltalkerLink) => void)[] = []; From 241f16f0b4dfcbc3fbc9a1f86c36c0acaf032ac9 Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 23 Sep 2024 17:23:39 +0200 Subject: [PATCH 11/14] added online indicator --- src/settings/components/ServiceSettings.tsx | 13 ++- src/walltalker/WalltalkerOnline.tsx | 54 +++++++++++++ src/walltalker/WalltalkerSearch.tsx | 63 ++++++--------- src/walltalker/WalltalkerService.ts | 89 ++++++++++++++------- src/walltalker/index.ts | 1 + 5 files changed, 149 insertions(+), 71 deletions(-) create mode 100644 src/walltalker/WalltalkerOnline.tsx diff --git a/src/settings/components/ServiceSettings.tsx b/src/settings/components/ServiceSettings.tsx index a372c383..dd3f83f0 100644 --- a/src/settings/components/ServiceSettings.tsx +++ b/src/settings/components/ServiceSettings.tsx @@ -1,8 +1,8 @@ import styled from 'styled-components'; -import { SettingsTile, TabBar } from '../../common'; +import { SettingsTile, Surrounded, TabBar } from '../../common'; import { E621Search } from '../../e621'; import { useState } from 'react'; -import { WalltalkerSearch } from '../../walltalker'; +import { WalltalkerOnline, WalltalkerSearch } from '../../walltalker'; import { LocalImport } from '../../local'; const tabs: Record = { @@ -29,7 +29,14 @@ export const ServiceSettings = () => { }> + Walltalker + + ), + }, { id: 'local', content: 'Device' }, ]} current={activeTab} diff --git a/src/walltalker/WalltalkerOnline.tsx b/src/walltalker/WalltalkerOnline.tsx new file mode 100644 index 00000000..6f2312ea --- /dev/null +++ b/src/walltalker/WalltalkerOnline.tsx @@ -0,0 +1,54 @@ +import styled, { keyframes } from 'styled-components'; +import { useWalltalker } from './WalltalkerProvider'; + +const StyledOnlineDotContainer = styled.div` + display: flex; + height: 1rem; + position: relative; +`; + +const StyledOnlineDot = styled.div<{ + $online?: boolean; +}>` + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: ${({ $online }) => ($online ? '#30AA65' : 'transparent')}; + border: ${({ $online }) => ($online ? 'transparent' : '2px solid grey')}; +`; + +const pulse = keyframes` + 0% { + transform: scale(1); + opacity: 0.4; + } + 100% { + transform: scale(2.5); + opacity: 0; + } +`; + +const PulseAnimation = styled.div` + position: absolute; + top: 0%; + left: 0%; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: #30aa65; + transform: translate(-50%, -50%); + animation: ${pulse} 2s infinite ease-out; +`; + +export const WalltalkerOnline = () => { + const { + data: { connected }, + } = useWalltalker(); + + return ( + + + {connected && } + + ); +}; diff --git a/src/walltalker/WalltalkerSearch.tsx b/src/walltalker/WalltalkerSearch.tsx index d470fcde..896b8021 100644 --- a/src/walltalker/WalltalkerSearch.tsx +++ b/src/walltalker/WalltalkerSearch.tsx @@ -1,4 +1,4 @@ -import { faSpinner, faWalkieTalkie } from '@fortawesome/free-solid-svg-icons'; +import { faRss, faSpinner } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ToggleTile, @@ -6,8 +6,6 @@ import { SettingsDescription, SettingsLabel, TextInput, - Button, - Spinner, } from '../common'; import styled from 'styled-components'; import { useWalltalker } from './WalltalkerProvider'; @@ -29,15 +27,16 @@ const StyledLinksForm = styled.div` `; export const WalltalkerSearch = () => { - const { service, settings: settingsRef, data } = useWalltalker(); - - const [settings, setSettings] = settingsRef; - const { connected } = data; + const { + service, + settings: [settings, setSettings], + data: { connected }, + } = useWalltalker(); const [searchLinks, setSearchLinks] = useState([]); - const [loading, setLoading] = useState(false); + const [, setLoading] = useState(false); - const search = useCallback(async () => { + const findLinks = useCallback(async () => { setLoading(true); if (!settings.username) return; const links = await service.getLinksFromUsername(settings.username); @@ -50,8 +49,22 @@ export const WalltalkerSearch = () => { Automatically add images from your Walltalker links! + + Username + { + setSettings({ ...settings, username: value }); + }} + onSubmit={findLinks} + placeholder='Enter walltaker username...' + style={{ gridColumn: '2 / -1' }} + disabled={!connected} + /> + { setSettings({ ...settings, enabled: !settings.enabled }); }} @@ -60,11 +73,11 @@ export const WalltalkerSearch = () => { settings.enabled && !connected ? ( ) : ( - + ) } > - Use Walltaker + Connect

Let others choose wallpapers for your session, live!

@@ -76,31 +89,7 @@ export const WalltalkerSearch = () => { style={{ gridColumn: '1 / -1' }} transition={defaultTransition} > - - Username - { - setSettings({ ...settings, username: value }); - }} - onSubmit={search} - placeholder='Enter walltaker username...' - style={{ gridColumn: '2 / -1' }} - disabled={!connected} - /> - - - Search results + Links {searchLinks.map(link => { return ( res()); this.socket.addEventListener('error', () => rej()); }); } + private pingEcho = (message: MessageEvent) => { + if (!this.socket) return; + const content = JSON.parse(message.data); + if (content.type === 'ping') { + this.socket.send(JSON.stringify({ type: 'pong' })); + } + }; + + private consumeUntil = async ( + predicate: (message: MessageEvent) => boolean, + timeout: number = 5000 + ) => { + return new Promise>((res, rej) => { + if (!this.socket) { + rej('No socket connected.'); + return; + } + + const timer = setTimeout(() => { + this.socket?.removeEventListener('message', handler); + rej('Timeout reached.'); + }, timeout); + + const handler = (message: MessageEvent) => { + if (predicate(message)) { + this.socket?.removeEventListener('message', handler); + clearTimeout(timer); + res(message); + } + }; + + this.socket.addEventListener('message', handler); + this.socket.addEventListener('close', () => rej('Socket closed.')); + }); + }; + disconnect(): Promise { return new Promise(res => { if (this.socket) { @@ -83,8 +120,6 @@ export class WalltalkerService { listenTo(id: number): Promise { return new Promise((resolve, reject) => { - const MAX_RETRIES = 4; - let tries = 0; const eventName = 'confirm_subscription'; if (!this.socket) { @@ -92,35 +127,6 @@ export class WalltalkerService { return; } - const handler = (message: MessageEvent) => { - if (!this.socket) return; - - const content = JSON.parse(message.data); - if (content.identifier) { - content.identifier = JSON.parse(content.identifier); - } - - if ( - content.type === eventName && - isEqual(content.identifier, this.channelIdentifierFor(id)) - ) { - console.log('Handshake confirmed for Link#' + id); - this.socket.removeEventListener('message', handler); - this.subscriptions.push(id); - resolve(); - return; - } - - if (++tries > MAX_RETRIES) { - this.socket.removeEventListener('message', handler); - reject( - `Max retries hit when waiting for handshake confirmation for Link#${id}.` - ); - } - }; - - this.socket.addEventListener('message', handler); - const channelId = this.channelIdentifierFor(id); const message = { command: 'subscribe', @@ -128,6 +134,27 @@ export class WalltalkerService { }; this.socket.send(JSON.stringify(message)); + + resolve( + this.consumeUntil((message: MessageEvent) => { + const content = JSON.parse(message.data); + if (content.identifier) { + content.identifier = JSON.parse(content.identifier); + } + + return ( + content.type === eventName && + isEqual(content.identifier, this.channelIdentifierFor(id)) + ); + }) + .then(() => { + console.log('Handshake confirmed for Link#' + id); + this.subscriptions.push(id); + }) + .catch(error => { + reject(`Failed to confirm handshake for Link#${id}: ${error}`); + }) + ); }); } diff --git a/src/walltalker/index.ts b/src/walltalker/index.ts index 2778046e..c092f622 100644 --- a/src/walltalker/index.ts +++ b/src/walltalker/index.ts @@ -1,3 +1,4 @@ +export * from './WalltalkerOnline'; export * from './WalltalkerProvider'; export * from './WalltalkerSearch'; export * from './WalltalkerService'; From 6d0f39a73af7d79278a70f965c70d4835c9dd7a0 Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 23 Sep 2024 18:40:15 +0200 Subject: [PATCH 12/14] talked to the walls --- src/home/HomePage.tsx | 4 +- .../{WallTalkerAd.tsx => WallTakerAd.tsx} | 10 ++-- src/home/components/index.ts | 2 +- src/index.tsx | 6 +- src/settings/components/ServiceSettings.tsx | 11 ++-- .../WalltakerOnline.tsx} | 6 +- .../WalltakerProvider.tsx} | 58 +++++++++---------- .../WalltakerSearch.tsx} | 18 +++--- .../WalltakerService.ts} | 20 +++---- src/walltaker/index.ts | 4 ++ src/walltalker/index.ts | 4 -- 11 files changed, 71 insertions(+), 72 deletions(-) rename src/home/components/{WallTalkerAd.tsx => WallTakerAd.tsx} (87%) rename src/{walltalker/WalltalkerOnline.tsx => walltaker/WalltakerOnline.tsx} (89%) rename src/{walltalker/WalltalkerProvider.tsx => walltaker/WalltakerProvider.tsx} (60%) rename src/{walltalker/WalltalkerSearch.tsx => walltaker/WalltakerSearch.tsx} (89%) rename src/{walltalker/WalltalkerService.ts => walltaker/WalltakerService.ts} (91%) create mode 100644 src/walltaker/index.ts delete mode 100644 src/walltalker/index.ts diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index ec1ea38f..df13921d 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { HomeTitle, - WallTalkerAd, + WallTakerAd, ReleaseNotes, AgeWarning, Introduction, @@ -34,7 +34,7 @@ export const HomePage = () => { - + diff --git a/src/home/components/WallTalkerAd.tsx b/src/home/components/WallTakerAd.tsx similarity index 87% rename from src/home/components/WallTalkerAd.tsx rename to src/home/components/WallTakerAd.tsx index d9e3f340..5e67cfa1 100644 --- a/src/home/components/WallTalkerAd.tsx +++ b/src/home/components/WallTakerAd.tsx @@ -2,7 +2,7 @@ import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { ContentSection } from '../../common'; -const StyledWallTalkerAd = styled(ContentSection)` +const StyledWallTakerAd = styled(ContentSection)` background: linear-gradient(#ffb300, #916706); color: #583c0f; @@ -31,7 +31,7 @@ const StyledWallTalkerAd = styled(ContentSection)` &::before { position: absolute; - content: 'Walltalker ✨'; + content: 'Walltaker ✨'; background-color: #000000b5; color: #e6e6e6; @@ -47,9 +47,9 @@ const StyledWallTalkerAd = styled(ContentSection)` } `; -export const WallTalkerAd = () => { +export const WallTakerAd = () => { return ( - +

Want to let other people set your wallpaper?

@@ -59,6 +59,6 @@ export const WallTalkerAd = () => { within your blacklist!

-
+ ); }; diff --git a/src/home/components/index.ts b/src/home/components/index.ts index aaae853f..fadcc069 100644 --- a/src/home/components/index.ts +++ b/src/home/components/index.ts @@ -6,4 +6,4 @@ export * from './Introduction'; export * from './ReleaseNotes'; export * from './StartButton'; export * from './VersionDisplay'; -export * from './WallTalkerAd'; +export * from './WallTakerAd'; diff --git a/src/index.tsx b/src/index.tsx index 91830a31..e727f28b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,20 +6,20 @@ import { SettingsProvider, ImageProvider } from './settings'; import { E621Provider } from './e621'; import { VibratorProvider } from './utils'; import { LocalImageProvider } from './local'; -import { WalltalkerProvider } from './walltalker'; +import { WalltakerProvider } from './walltaker'; ReactDOM.createRoot(document.getElementById('root')!).render( - + - + diff --git a/src/settings/components/ServiceSettings.tsx b/src/settings/components/ServiceSettings.tsx index dd3f83f0..945df93f 100644 --- a/src/settings/components/ServiceSettings.tsx +++ b/src/settings/components/ServiceSettings.tsx @@ -2,12 +2,13 @@ import styled from 'styled-components'; import { SettingsTile, Surrounded, TabBar } from '../../common'; import { E621Search } from '../../e621'; import { useState } from 'react'; -import { WalltalkerOnline, WalltalkerSearch } from '../../walltalker'; + import { LocalImport } from '../../local'; +import { WalltakerSearch, WalltakerOnline } from '../../walltaker'; const tabs: Record = { e621: , - walltalker: , + walltaker: , local: , }; @@ -30,10 +31,10 @@ export const ServiceSettings = () => { tabs={[ { id: 'e621', content: 'e621' }, { - id: 'walltalker', + id: 'walltaker', content: ( - }> - Walltalker + }> + Walltaker ), }, diff --git a/src/walltalker/WalltalkerOnline.tsx b/src/walltaker/WalltakerOnline.tsx similarity index 89% rename from src/walltalker/WalltalkerOnline.tsx rename to src/walltaker/WalltakerOnline.tsx index 6f2312ea..512cca44 100644 --- a/src/walltalker/WalltalkerOnline.tsx +++ b/src/walltaker/WalltakerOnline.tsx @@ -1,5 +1,5 @@ import styled, { keyframes } from 'styled-components'; -import { useWalltalker } from './WalltalkerProvider'; +import { useWalltaker } from './WalltakerProvider'; const StyledOnlineDotContainer = styled.div` display: flex; @@ -40,10 +40,10 @@ const PulseAnimation = styled.div` animation: ${pulse} 2s infinite ease-out; `; -export const WalltalkerOnline = () => { +export const WalltakerOnline = () => { const { data: { connected }, - } = useWalltalker(); + } = useWalltaker(); return ( diff --git a/src/walltalker/WalltalkerProvider.tsx b/src/walltaker/WalltakerProvider.tsx similarity index 60% rename from src/walltalker/WalltalkerProvider.tsx rename to src/walltaker/WalltakerProvider.tsx index 2bfefd9e..6081afe6 100644 --- a/src/walltalker/WalltalkerProvider.tsx +++ b/src/walltaker/WalltakerProvider.tsx @@ -9,40 +9,40 @@ import { useState, } from 'react'; import { createLocalStorageProvider, StateAndSetter } from '../utils'; -import { WalltalkerLink, WalltalkerService } from './WalltalkerService'; +import { WalltakerLink, WalltakerService } from './WalltakerService'; import { uniqBy } from 'lodash'; import { useImages } from '../settings'; import { ImageServiceType, ImageType } from '../types'; -export interface WalltalkerSettings { +export interface WalltakerSettings { enabled?: boolean; username?: string; ids?: number[]; } -const walltalkerStorageKey = 'walltalker'; +const walltakerStorageKey = 'walltaker'; const { - Provider: WalltalkerSettingsProvider, - useProvider: useWalltalkerSettings, -} = createLocalStorageProvider({ - key: walltalkerStorageKey, + Provider: WalltakerSettingsProvider, + useProvider: useWalltakerSettings, +} = createLocalStorageProvider({ + key: walltakerStorageKey, defaultData: {}, }); -const WalltalkerServiceProvider: React.FC = ({ +const WalltakerServiceProvider: React.FC = ({ children, }) => { - const settingsRef = useWalltalkerSettings(); - const service = useMemo(() => new WalltalkerService(), []); + const settingsRef = useWalltakerSettings(); + const service = useMemo(() => new WalltakerService(), []); const [, setImages] = useImages(); const [settings] = settingsRef; - const [data, setData] = useState({}); + const [data, setData] = useState({}); const onLink = useCallback( - (link: WalltalkerLink) => { + (link: WalltakerLink) => { setData(prev => ({ ...prev, links: uniqBy([link, ...(prev.links ?? [])], 'id'), @@ -97,42 +97,40 @@ const WalltalkerServiceProvider: React.FC = ({ }, [data.connected, service, settings.enabled, settings.ids]); return ( - + {children} - + ); }; -export interface WalltalkerData { +export interface WalltakerData { connected?: boolean; - links?: WalltalkerLink[]; + links?: WalltakerLink[]; } -export interface WalltalkerContext { - settings: StateAndSetter; - service: WalltalkerService; - data: WalltalkerData; +export interface WalltakerContext { + settings: StateAndSetter; + service: WalltakerService; + data: WalltakerData; } -const WalltalkerContext = createContext(null); +const WalltakerContext = createContext(null); -export const WalltalkerProvider: React.FC = ({ +export const WalltakerProvider: React.FC = ({ children, }) => { return ( - - {children} - + + {children} + ); }; -export const useWalltalker = (): WalltalkerContext => { - const context = useContext(WalltalkerContext); +export const useWalltaker = (): WalltakerContext => { + const context = useContext(WalltakerContext); if (!context) { - throw new Error('useWalltalker must be used within a WalltalkerProvider'); + throw new Error('useWalltaker must be used within a WalltakerProvider'); } return context; diff --git a/src/walltalker/WalltalkerSearch.tsx b/src/walltaker/WalltakerSearch.tsx similarity index 89% rename from src/walltalker/WalltalkerSearch.tsx rename to src/walltaker/WalltakerSearch.tsx index 896b8021..5f96c458 100644 --- a/src/walltalker/WalltalkerSearch.tsx +++ b/src/walltaker/WalltakerSearch.tsx @@ -8,13 +8,13 @@ import { TextInput, } from '../common'; import styled from 'styled-components'; -import { useWalltalker } from './WalltalkerProvider'; +import { useWalltaker } from './WalltakerProvider'; import { motion } from 'framer-motion'; import { defaultTransition } from '../utils'; import { useCallback, useState } from 'react'; -import { WalltalkerLink } from './WalltalkerService'; +import { WalltakerLink } from './WalltakerService'; -const StyledWalltalkerSearch = styled.div` +const StyledWalltakerSearch = styled.div` display: grid; grid-template-columns: auto 1fr auto; `; @@ -26,14 +26,14 @@ const StyledLinksForm = styled.div` gap: 0.75rem; `; -export const WalltalkerSearch = () => { +export const WalltakerSearch = () => { const { service, settings: [settings, setSettings], data: { connected }, - } = useWalltalker(); + } = useWalltaker(); - const [searchLinks, setSearchLinks] = useState([]); + const [searchLinks, setSearchLinks] = useState([]); const [, setLoading] = useState(false); const findLinks = useCallback(async () => { @@ -45,9 +45,9 @@ export const WalltalkerSearch = () => { }, [service, settings.username]); return ( - + - Automatically add images from your Walltalker links! + Automatically add images from your Walltaker links! Username @@ -118,6 +118,6 @@ export const WalltalkerSearch = () => { })} )} - + ); }; diff --git a/src/walltalker/WalltalkerService.ts b/src/walltaker/WalltakerService.ts similarity index 91% rename from src/walltalker/WalltalkerService.ts rename to src/walltaker/WalltakerService.ts index 7014283e..60a99286 100644 --- a/src/walltalker/WalltalkerService.ts +++ b/src/walltaker/WalltakerService.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance } from 'axios'; import { isEqual } from 'lodash'; -export interface WalltalkerLink { +export interface WalltakerLink { id: number; expires: string; terms: string; @@ -17,16 +17,16 @@ export interface WalltalkerLink { online: boolean; } -export interface WalltalkerUser { +export interface WalltakerUser { username: string; id: number; set_count: number; online: boolean; authenticated: boolean; - links: WalltalkerLink[]; + links: WalltakerLink[]; } -export class WalltalkerService { +export class WalltakerService { constructor() { this.axiosInstance = axios.create({ baseURL: 'https://walltaker.joi.how/api', @@ -183,13 +183,13 @@ export class WalltalkerService { async getLink(id: number) { return this.axiosInstance - .get(`/links/${id}.json`) + .get(`/links/${id}.json`) .then(res => res.data); } async getLinksFromUsername(username: string) { return this.axiosInstance - .get(`/users/${username}.json`) + .get(`/users/${username}.json`) .then(res => res.data.links); } @@ -197,19 +197,19 @@ export class WalltalkerService { const content = JSON.parse(message.data); if (content.message?.success === true && content.message?.post_url) { this.listeners.forEach(listener => - listener(content.message as WalltalkerLink) + listener(content.message as WalltakerLink) ); } console.log(content); } - private listeners: ((link: WalltalkerLink) => void)[] = []; + private listeners: ((link: WalltakerLink) => void)[] = []; - addLinkListener(callback: (link: WalltalkerLink) => void) { + addLinkListener(callback: (link: WalltakerLink) => void) { this.listeners.push(callback); } - removeLinkListener(callback: (link: WalltalkerLink) => void) { + removeLinkListener(callback: (link: WalltakerLink) => void) { this.listeners = this.listeners.filter(listener => listener !== callback); } } diff --git a/src/walltaker/index.ts b/src/walltaker/index.ts new file mode 100644 index 00000000..5dbaf555 --- /dev/null +++ b/src/walltaker/index.ts @@ -0,0 +1,4 @@ +export * from './WalltakerOnline'; +export * from './WalltakerProvider'; +export * from './WalltakerService'; +export * from './WalltakerSearch'; diff --git a/src/walltalker/index.ts b/src/walltalker/index.ts deleted file mode 100644 index c092f622..00000000 --- a/src/walltalker/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './WalltalkerOnline'; -export * from './WalltalkerProvider'; -export * from './WalltalkerSearch'; -export * from './WalltalkerService'; From fa073c71ac2ff166b94e0dfffc8ed8ae83f0702d Mon Sep 17 00:00:00 2001 From: clragon Date: Mon, 23 Sep 2024 19:04:22 +0200 Subject: [PATCH 13/14] added walltaker mascot --- src/home/components/WallTakerAd.tsx | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/home/components/WallTakerAd.tsx b/src/home/components/WallTakerAd.tsx index 5e67cfa1..57feea5e 100644 --- a/src/home/components/WallTakerAd.tsx +++ b/src/home/components/WallTakerAd.tsx @@ -27,29 +27,24 @@ const StyledWallTakerAd = styled(ContentSection)` & a { text-decoration: none; } +`; - &::before { - position: absolute; - - content: 'Walltaker ✨'; - - background-color: #000000b5; - color: #e6e6e6; - text-align: center; - - right: -20px; - bottom: 120px; - width: 200px; - height: 20px; - - transform: translateY(50%) rotate(-45deg); - transform-origin: top right; - } +const StyledWalltakerMascotBackground = styled.div` + bottom: -50px; + right: -40px; + height: 180px; + aspect-ratio: 1/1; + position: absolute; + background-image: url('https://walltaker.joi.how/assets/mascot/TaylorSFW-1f4700509acff90902c73b80246473840a4879dca17a0052e0d8a41b1e4556e2.png'); + background-repeat: no-repeat; + background-size: contain; + background-position: right bottom; `; export const WallTakerAd = () => { return ( +

Want to let other people set your wallpaper?

From e8b9c51a81c982a772389002c84a84561892432d Mon Sep 17 00:00:00 2001 From: clragon Date: Sun, 29 Sep 2024 15:38:10 +0200 Subject: [PATCH 14/14] added walltaker credential input --- README.md | 2 + src/e621/E621Credentials.tsx | 22 +-- src/e621/E621Search.tsx | 2 +- src/local/LocalImport.tsx | 4 +- src/walltaker/WalltakerCredentials.tsx | 104 ++++++++++ src/walltaker/WalltakerProvider.tsx | 7 +- src/walltaker/WalltakerSearch.tsx | 259 ++++++++++++++++--------- src/walltaker/WalltakerService.ts | 13 +- 8 files changed, 302 insertions(+), 111 deletions(-) create mode 100644 src/walltaker/WalltakerCredentials.tsx diff --git a/README.md b/README.md index 4c6aa8f0..950dafd2 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,9 @@ yarn yarn build yarn preview ``` + 4. open one of the URLs listed in your browser + ### Social - We have an [e6 forum thread](https://e621.net/forum_topics/23796). We'd love to hear your feedback. diff --git a/src/e621/E621Credentials.tsx b/src/e621/E621Credentials.tsx index 106b8675..d7ce609a 100644 --- a/src/e621/E621Credentials.tsx +++ b/src/e621/E621Credentials.tsx @@ -2,6 +2,7 @@ import styled from 'styled-components'; import { E621Service } from './E621Service'; import { useCallback, useMemo, useState } from 'react'; import { + Button, SettingsInfo, SettingsLabel, Space, @@ -30,21 +31,6 @@ const StyledE621SaveCredentials = styled.div` align-items: center; `; -const StyledE621SaveCredentialsButton = styled.button` - background: var(--button-background); - color: var(--button-color); - border-radius: var(--border-radius); - padding: 4px 8px; - transition: - background 0.2s, - opacity 0.2s; - cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; - &:hover { - background: var(--primary); - } - opacity: ${props => (props.disabled ? 0.5 : 1)}; -`; - export const E621CredentialsInput = ({ service, initialValue, @@ -85,6 +71,7 @@ export const E621CredentialsInput = ({ Username @@ -100,6 +87,7 @@ export const E621CredentialsInput = ({ API Key @@ -117,12 +105,12 @@ export const E621CredentialsInput = ({ {loading && } - Save - + ); diff --git a/src/e621/E621Search.tsx b/src/e621/E621Search.tsx index 0a2096d3..fba2092a 100644 --- a/src/e621/E621Search.tsx +++ b/src/e621/E621Search.tsx @@ -94,7 +94,7 @@ export const E621Search = () => { return ( - Add images from e621 + Add images from e621's search Tags { return ( - Add images from your device + + Add images from your device storage + ; + onSaved?: (credentials: WalltakerCredentials) => void; + disabled?: boolean; +} + +const StyledE621CredentialsInput = styled.div` + grid-column: 1 / -1; + display: grid; + grid-template-columns: auto 1fr auto; +`; + +const StyledE621SaveCredentials = styled.div` + grid-column: 1 / -1; + display: flex; + justify-content: flex-end; + align-items: center; +`; + +export const WalltakerCredentialsInput = ({ + service, + initialValue, + onSaved, + disabled, +}: WalltakerCredentialsInputProps) => { + const [input, setInput] = useState>({ + ...initialValue, + username: '', + apiKey: '', + }); + const [loading, setLoading] = useState(false); + + const onSave = useCallback(async () => { + const credentials = input as WalltakerCredentials; + setLoading(true); + const valid = await service.testCredentials(credentials); + if (valid) { + onSaved?.(credentials); + } + }, [input, onSaved, service]); + + const hasData = useMemo( + () => input?.username && input?.apiKey, + [input?.username, input?.apiKey] + ); + + const locked = useMemo(() => loading || disabled, [loading, disabled]); + + return ( + + + Username + + setInput({ + ...input, + username, + }) + } + placeholder='Username' + autoComplete='username' + disabled={locked} + /> + + API Key + + setInput({ + ...input, + apiKey, + }) + } + placeholder='API Key' + type='password' + autoComplete='current-password' + disabled={locked} + /> + + + {loading && } + + + + + ); +}; diff --git a/src/walltaker/WalltakerProvider.tsx b/src/walltaker/WalltakerProvider.tsx index 6081afe6..32c6eab3 100644 --- a/src/walltaker/WalltakerProvider.tsx +++ b/src/walltaker/WalltakerProvider.tsx @@ -14,9 +14,14 @@ import { uniqBy } from 'lodash'; import { useImages } from '../settings'; import { ImageServiceType, ImageType } from '../types'; +export interface WalltakerCredentials { + username: string; + apiKey?: string; +} + export interface WalltakerSettings { enabled?: boolean; - username?: string; + credentials?: WalltakerCredentials; ids?: number[]; } diff --git a/src/walltaker/WalltakerSearch.tsx b/src/walltaker/WalltakerSearch.tsx index 5f96c458..3e8d14b4 100644 --- a/src/walltaker/WalltakerSearch.tsx +++ b/src/walltaker/WalltakerSearch.tsx @@ -1,31 +1,33 @@ -import { faRss, faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { + faCheckSquare, + faMinusSquare, + faRightFromBracket, + faRss, + faSpinner, + faUser, +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ToggleTile, ToggleTileType, SettingsDescription, - SettingsLabel, - TextInput, + Surrounded, + IconButton, } from '../common'; import styled from 'styled-components'; import { useWalltaker } from './WalltakerProvider'; -import { motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'framer-motion'; import { defaultTransition } from '../utils'; -import { useCallback, useState } from 'react'; +import { useEffect, useState } from 'react'; import { WalltakerLink } from './WalltakerService'; +import { WalltakerCredentialsInput } from './WalltakerCredentials'; +import { faSquare } from '@fortawesome/free-regular-svg-icons'; const StyledWalltakerSearch = styled.div` display: grid; grid-template-columns: auto 1fr auto; `; -const StyledLinksForm = styled.div` - grid-column: 1 / -1; - display: grid; - grid-template-columns: auto 1fr auto; - gap: 0.75rem; -`; - export const WalltakerSearch = () => { const { service, @@ -33,91 +35,170 @@ export const WalltakerSearch = () => { data: { connected }, } = useWalltaker(); - const [searchLinks, setSearchLinks] = useState([]); - const [, setLoading] = useState(false); + const [allLinks, setAllLinks] = useState([]); + const [loading] = useState(false); - const findLinks = useCallback(async () => { - setLoading(true); - if (!settings.username) return; - const links = await service.getLinksFromUsername(settings.username); - setSearchLinks(links); - setLoading(false); - }, [service, settings.username]); + useEffect(() => { + if (settings.credentials) { + service.getUserLinks(settings.credentials).then(links => { + setAllLinks(links); + }); + } + }, [service, settings]); return ( - Automatically add images from your Walltaker links! + Let other users add images to your session live! - - Username - { - setSettings({ ...settings, username: value }); - }} - onSubmit={findLinks} - placeholder='Enter walltaker username...' - style={{ gridColumn: '2 / -1' }} - disabled={!connected} - /> - - { - setSettings({ ...settings, enabled: !settings.enabled }); - }} - type={ToggleTileType.radio} - trailing={ - settings.enabled && !connected ? ( - - ) : ( - - ) - } - > - Connect -

Let others choose wallpapers for your session, live!

-
- - {connected && ( - - Links - {searchLinks.map(link => { - return ( - { - if (settings.ids?.includes(link.id)) { - setSettings({ - ...settings, - ids: settings.ids?.filter(id => id !== link.id), - }); - } else { - setSettings({ - ...settings, - ids: [...(settings.ids ?? []), link.id], - }); + + {settings.credentials ? ( + <> + } + onClick={() => { + setSettings({ ...settings, credentials: undefined }); + }} + style={{ + padding: '10px', + }} + /> + } + > + + } - }} - type={ToggleTileType.radio} - > - #{link.id} - {/* dangerously set inner html */} -

+ > + {settings.credentials.username} + +

You are connected

- ); - })} -
- )} + + { + setSettings({ ...settings, enabled: !settings.enabled }); + }} + type={ToggleTileType.radio} + trailing={ + settings.enabled && !connected ? ( + + ) : ( + + ) + } + > + Enable +

Receive images from other users for this session

+
+ + {settings.enabled && ( + + + 0 + ? 'Deselect all links' + : 'Select all links' + } + icon={ + { + if ( + settings.ids && + allLinks.length > 0 && + settings.ids.length === allLinks.length + ) { + return faCheckSquare; + } else if ( + settings.ids && + settings.ids.length > 0 + ) { + return faMinusSquare; + } else { + return faSquare; + } + })()} + /> + } + onClick={() => { + if ( + settings.ids && + settings.ids.length === allLinks.length + ) { + setSettings({ ...settings, ids: [] }); + } else { + setSettings({ + ...settings, + ids: allLinks.map(link => link.id), + }); + } + }} + /> + } + > + Links + + + {allLinks.map(link => { + return ( + { + if (settings.ids?.includes(link.id)) { + setSettings({ + ...settings, + ids: settings.ids?.filter(id => id !== link.id), + }); + } else { + setSettings({ + ...settings, + ids: [...(settings.ids ?? []), link.id], + }); + } + }} + type={ToggleTileType.check} + > + #{link.id} +

+ + ); + })} + + )} + + ) : ( + { + setSettings({ ...settings, credentials }); + }} + disabled={loading} + /> + )} + ); }; diff --git a/src/walltaker/WalltakerService.ts b/src/walltaker/WalltakerService.ts index 60a99286..936b146a 100644 --- a/src/walltaker/WalltakerService.ts +++ b/src/walltaker/WalltakerService.ts @@ -1,5 +1,6 @@ import axios, { AxiosInstance } from 'axios'; import { isEqual } from 'lodash'; +import { WalltakerCredentials } from './WalltakerProvider'; export interface WalltakerLink { id: number; @@ -187,9 +188,17 @@ export class WalltakerService { .then(res => res.data); } - async getLinksFromUsername(username: string) { + async getUserLinks(credentials: WalltakerCredentials) { return this.axiosInstance - .get(`/users/${username}.json`) + .get(`/users/${credentials.username}.json`, { + params: { + api_key: credentials.apiKey, + }, + headers: { + // Something like this; double check with Gray + // Authorization: `Bearer ${credentials.apiKey}`, + }, + }) .then(res => res.data.links); }