mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-04-10 14:11:22 -06:00
Merge branch 'main' into enhance-list-features
This commit is contained in:
commit
3d1d291ffa
2
.gitignore
vendored
2
.gitignore
vendored
@ -46,3 +46,5 @@ publish_keys.json
|
||||
docs/.vitepress/cache
|
||||
|
||||
project.zip
|
||||
|
||||
*storybook.log
|
||||
|
@ -3,17 +3,29 @@ import type { StorybookConfig } from "@storybook/react-vite";
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-essentials",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-dark-mode",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
viteFinal: (config) => {
|
||||
config.define = {
|
||||
...config.define,
|
||||
"process.env": process.env,
|
||||
esbuild: {
|
||||
jsx: "automatic",
|
||||
},
|
||||
};
|
||||
return config;
|
||||
},
|
||||
typescript: {
|
||||
reactDocgen: "react-docgen-typescript",
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import '../src/style.content.css';
|
||||
import "../src/style.content.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
@ -11,6 +10,8 @@ const preview: Preview = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
@ -47,7 +47,7 @@ https://github.com/kawamataryo/sky-follower-bridge/assets/11070996/67bdd228-dc67
|
||||
|
||||
## 🚨 Limitations
|
||||
|
||||
- User search may fail due to late limit in Bluesky's API. In this case, please wait for 2 to 3 minutes and execute the search again.
|
||||
- User search may fail due to rate limit in Bluesky's API. In this case, please wait for 2 to 3 minutes and execute the search again.
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -26,7 +26,7 @@ export default defineConfig({
|
||||
description: "Sky Follower Bridge is a Chrome extension that allows you to follow users on Bluesky from your own account.",
|
||||
themeConfig: {
|
||||
logo: {
|
||||
src: "/images/logo.png",
|
||||
src: "/images/logo.webp",
|
||||
alt: "Sky Follower Bridge Logo",
|
||||
},
|
||||
|
||||
@ -49,6 +49,10 @@ export default defineConfig({
|
||||
icon: "x",
|
||||
link: "https://x.com/KawamataRyo",
|
||||
},
|
||||
{
|
||||
icon: "kofi",
|
||||
link: "https://ko-fi.com/kawamataryo",
|
||||
},
|
||||
],
|
||||
|
||||
outline: {
|
||||
@ -138,6 +142,18 @@ export default defineConfig({
|
||||
{ text: "Guia de Solução de Problemas", link: "/pt/troubleshooting" },
|
||||
],
|
||||
}
|
||||
},
|
||||
es: {
|
||||
label: "Español",
|
||||
lang: "es",
|
||||
link: "/es/",
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: "Inicio", link: "/" },
|
||||
{ text: "Comenzando", link: "/es/get-started" },
|
||||
{ text: "Guía de solución de problemas", link: "/es/troubleshooting" },
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -11,8 +11,15 @@
|
||||
}
|
||||
|
||||
.VPImage.image-src {
|
||||
max-width: 180px;
|
||||
max-height: 180px;
|
||||
max-width: 130px;
|
||||
max-height: 130px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.VPImage.image-src {
|
||||
max-width: 170px;
|
||||
max-height: 170px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
87
docs/es/get-started.md
Normal file
87
docs/es/get-started.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Comenzando
|
||||
|
||||
Sky Follower Bridge te ayuda a encontrar y seguir tus conexiones de 𝕏 (Twitter) en Bluesky.
|
||||
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/dfMK07PJeL4?si=SDC7P8basmoOOdjw" title="Reproductor de video de YouTube" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
|
||||
## Instalación
|
||||
|
||||
Sky Follower Bridge está disponible en:
|
||||
|
||||
- [Chrome Web Store](https://chrome.google.com/webstore/detail/sky-follower-bridge/behhbpbpmailcnfbjagknjngnfdojpko) (Recomendado)
|
||||
- [Complementos de Firefox](https://addons.mozilla.org/en-US/firefox/addon/sky-follower-bridge/)
|
||||
- [Complementos de Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/sky-follower-bridge/dpeolmdblhfolkhlhbhlofkkpaojnnbb)
|
||||
|
||||
::: tip
|
||||
Recomendamos usar la versión de Chrome Web Store ya que siempre está actualizada. Las versiones de otras tiendas pueden retrasarse en las actualizaciones.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Sky Follower Bridge solo está disponible para navegadores de escritorio. Los navegadores móviles no son compatibles.
|
||||
:::
|
||||
|
||||
## Uso
|
||||
|
||||
### 1. Navega a 𝕏 (Twitter)
|
||||
|
||||
Visita cualquiera de estas páginas en X:
|
||||
- Tu página de Seguidos: [x.com/following](https://x.com/following)
|
||||
- Tu página de Usuarios bloqueados: [x.com/settings/blocked/all](https://x.com/settings/blocked/all)
|
||||
- La página de Miembros de una Lista pública: `x.com/i/lists/<list_id>/members`
|
||||
|
||||

|
||||
|
||||
### 2. Inicia Sky Follower Bridge
|
||||
|
||||
Presiona `Alt + B` o haz clic en el ícono de la extensión en la barra de herramientas de tu navegador.
|
||||
|
||||
::: tip
|
||||
Para los usuarios de Firefox, presionar `Alt + B` puede no funcionar. En ese caso, haz clic en el ícono de la extensión en la barra de herramientas del navegador.
|
||||
|
||||
https://support.mozilla.org/en-US/kb/extensions-button
|
||||
:::
|
||||
|
||||

|
||||
|
||||
### 3. Inicia sesión en Bluesky
|
||||
|
||||
Ingresa tu identificador de Bluesky (o correo electrónico) y [Contraseña de la aplicación](https://bsky.app/settings/app-passwords).
|
||||
|
||||
::: tip
|
||||
Si encuentras errores de inicio de sesión, consulta la [Guía de solución de problemas](/troubleshooting).
|
||||
:::
|
||||
|
||||

|
||||
|
||||
### 4. Inicia la búsqueda
|
||||
|
||||
Haz clic en "Buscar usuarios de Bluesky" para comenzar a escanear. La extensión buscará perfiles de Bluesky coincidentes verificando la API de Bluesky.
|
||||
|
||||

|
||||
|
||||
### 5. Revisa los resultados
|
||||
|
||||
Haz clic en "Ver resultados" para ver las posibles coincidencias encontradas en Bluesky.
|
||||
|
||||

|
||||
|
||||
Esto abrirá la página de opciones mostrando todos los usuarios de Bluesky detectados.
|
||||
|
||||

|
||||
|
||||
### 6. Sigue a los usuarios
|
||||
|
||||
Haz clic en el botón "Seguir" junto a cualquier usuario con el que desees conectarte.
|
||||
|
||||

|
||||
|
||||
o usa el botón "Seguir a todos" para seguir a todos los usuarios detectados de una vez.
|
||||
|
||||

|
||||
|
||||
::: warning
|
||||
Ten en cuenta que el proceso de coincidencia no es perfecto y puede sugerir coincidencias incorrectas ocasionalmente. Siempre verifica el perfil antes de seguir.
|
||||
:::
|
||||
|
||||
¡Eso es todo! Disfruta conectándote con tu comunidad en Bluesky 🎉
|
32
docs/es/index.md
Normal file
32
docs/es/index.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "Sky Follower Bridge"
|
||||
text: "Conecta tus redes sociales"
|
||||
tagline: Migra sin problemas tus conexiones sociales de 𝕏 a Bluesky
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Comenzar
|
||||
link: /es/get-started
|
||||
- theme: alt
|
||||
text: Solución de problemas
|
||||
link: /es/troubleshooting
|
||||
image:
|
||||
src: /images/logo.webp
|
||||
alt: Imagen de portada de Sky Follower Bridge
|
||||
|
||||
features:
|
||||
- icon: 🔍
|
||||
title: Detección automática de perfiles
|
||||
details: Detecta automáticamente usuarios de Bluesky similares a tus seguidos en 𝕏.
|
||||
- icon: 🚀
|
||||
title: Función de seguimiento masivo
|
||||
details: Ahorra tiempo siguiendo a múltiples usuarios a la vez con nuestro botón "Seguir a todos".
|
||||
- icon: 📋
|
||||
title: Soporte para múltiples listas
|
||||
details: Funciona con listas de Seguidos, Seguidores, Usuarios bloqueados e incluso listas públicas de 𝕏.
|
||||
- icon: 🌐
|
||||
title: Soporte multiplataforma
|
||||
details: Disponible en Chrome, Firefox y Microsoft Edge para tu conveniencia.
|
||||
---
|
128
docs/es/troubleshooting.md
Normal file
128
docs/es/troubleshooting.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Guía de solución de problemas
|
||||
|
||||
## Errores de autenticación
|
||||
|
||||
### Problemas de inicio de sesión
|
||||
|
||||
**Mensaje de error:**
|
||||
<span class="error-message">Error: Invalid identifier or password</span>
|
||||
|
||||
**Lista de verificación:**
|
||||
1. Entrada de nombre de usuario y contraseña
|
||||
- Verifica si hay espacios accidentales
|
||||
- Si copias y pegas, asegúrate de que no se incluyan caracteres adicionales
|
||||
|
||||
2. Formato del nombre de usuario
|
||||
- Formato correcto: `tu-usuario.bsky.social`
|
||||
- Error común: `tu-usuario` (falta .bsky.social)
|
||||
|
||||
3. Información de la contraseña
|
||||
- Recomendamos encarecidamente usar una [Contraseña de la aplicación](https://bsky.app/settings/app-passwords) en lugar de tu contraseña regular
|
||||
- Formato de la contraseña de la aplicación: `xxxx-xxxx-xxxx-xxxx` (19 caracteres)
|
||||
|
||||
::: tip Consejos útiles
|
||||
No confundas la Contraseña de la aplicación con el "nombre de la contraseña" que se muestra en la configuración.
|
||||
Cómo crear una nueva Contraseña de la aplicación:
|
||||
2. [Navega a la sección de Contraseñas de la aplicación](https://bsky.app/settings/app-passwords)
|
||||
3. Haz clic en "Agregar Contraseña de la aplicación"
|
||||
4. Haz clic en "Crear Contraseña de la aplicación"
|
||||
4. Copia la contraseña generada de 19 caracteres
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### Se requiere autenticación de dos factores
|
||||
|
||||
**Mensaje de error:**
|
||||
<span class="error-message">Error: Two-factor authentication required</span>
|
||||
|
||||
**Solución:**
|
||||
1. Revisa tu correo electrónico para obtener el código de autenticación
|
||||
2. Ingresa el código en el campo de entrada de 2FA
|
||||
3. Intenta iniciar sesión nuevamente
|
||||
|
||||
## Errores de límite de tasa
|
||||
|
||||
**Mensaje de error:**
|
||||
<span class="error-message">Error: Rate limit error</span>
|
||||
|
||||
**Solución:**
|
||||
1. La API de Bluesky tiene los siguientes límites ([documentación oficial](https://docs.bsky.app/docs/advanced-guides/rate-limits)):
|
||||
- Hasta 5,000 puntos por hora (aproximadamente 1,666 acciones nuevas)
|
||||
- Hasta 35,000 puntos por día
|
||||
- Puntos por acción:
|
||||
- Crear: 3 puntos
|
||||
- Actualizar: 2 puntos
|
||||
- Eliminar: 1 punto
|
||||
2. Si alcanzas el límite, espera hasta que se restablezca
|
||||
3. Haz clic en el botón "Reiniciar" para intentarlo de nuevo
|
||||
|
||||
::: warning
|
||||
La versión publicada en Firefox frecuentemente encuentra errores de límite de tasa. Si encuentras un error, intenta en Chrome.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
La mayoría de los usuarios no alcanzarán estos límites durante el uso normal. Sin embargo, ten cuidado al realizar acciones masivas como seguir a muchos usuarios o dar me gusta a muchas publicaciones en un corto período.
|
||||
:::
|
||||
|
||||
## Errores de página
|
||||
|
||||
### Página inválida
|
||||
|
||||
**Mensaje de error:**
|
||||
<span class="error-message">Error: Invalid page. please open the 𝕏 following or blocking or list page.</span>
|
||||
|
||||
**Solución:**
|
||||
Usa la extensión solo en estas páginas de 𝕏 (Twitter):
|
||||
- Página de seguidos ([x.com/following](https://x.com/following))
|
||||
- Página de bloqueados ([x.com/settings/blocked/all](https://x.com/settings/blocked/all))
|
||||
- Página de miembros de lista (`x.com/i/lists/<list_id>/members`)
|
||||
|
||||
o verifica los permisos de tu extensión en la página de extensiones.
|
||||
Los permisos del sitio deben ser como se muestra a continuación:
|
||||
|
||||
<img src="/images/site_permissions.png" alt="permisos del sitio" width="500"/>
|
||||
|
||||
## Problemas de escaneo
|
||||
|
||||
### El botón View Detected Users no funciona
|
||||
|
||||
Por alguna razón, el botón View Detected Users puede no funcionar.
|
||||
|
||||
**Solución:**
|
||||
1. Haz clic derecho en el ícono de la extensión y selecciona "Opciones"
|
||||
2. Se mostrará la página de resultados
|
||||
|
||||
<img src="/images/click-option.png" alt="hacer clic en opción" width="500"/>
|
||||
|
||||
### El escaneo se detiene temprano
|
||||
|
||||
El escaneo se detiene antes de llegar al final de la página
|
||||
|
||||
**Solución:**
|
||||
1. Haz clic en "Reanudar escaneo" para continuar
|
||||
2. El escaneo se detendrá automáticamente cuando llegue al final de la página
|
||||
3. Puedes hacer clic en "Detener escaneo y ver resultados" en cualquier momento
|
||||
|
||||
### No se encontraron usuarios
|
||||
|
||||
No se detectaron usuarios de Bluesky después del escaneo
|
||||
|
||||
**Solución:**
|
||||
1. Asegúrate de haber iniciado sesión correctamente
|
||||
2. Intenta escanear de nuevo - algunos usuarios pueden no ser detectados en el primer intento
|
||||
3. Verifica si los usuarios de 𝕏 han vinculado sus cuentas de Bluesky en sus perfiles
|
||||
|
||||
## Otros problemas
|
||||
|
||||
Si encuentras errores inesperados:
|
||||
|
||||
1. Recarga la página
|
||||
2. Intenta la operación nuevamente
|
||||
3. Si el problema persiste, puedes:
|
||||
- [Crear un problema](https://github.com/kawamataryo/sky-follower-bridge/issues) con:
|
||||
- El mensaje de error exacto
|
||||
- Lo que estabas intentando hacer
|
||||
- Tu tipo y versión de navegador
|
||||
- Cualquier captura de pantalla relevante
|
||||
- O mencionar a [@kawamataryo.bsky.social](https://bsky.app/profile/kawamataryo.bsky.social) en Bluesky
|
@ -4,7 +4,6 @@ Sky Follower Bridge vous aide à trouver et suivre vos connexions 𝕏 (Twitter)
|
||||
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/dfMK07PJeL4?si=SDC7P8basmoOOdjw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
<p style="color: gray; font-size: 0.8em; line-height: 1.4;">Cette vidéo démontre la version Edge. La dernière version de Chrome et les versions récentes de Firefox peuvent se comporter différemment. Un tutoriel vidéo pour Chrome arrive bientôt.</p>
|
||||
|
||||
## Installation
|
||||
|
||||
@ -18,6 +17,10 @@ Sky Follower Bridge est disponible sur :
|
||||
Nous recommandons d'utiliser la version du Chrome Web Store car elle est toujours à jour. Les versions des autres magasins peuvent être en retard dans les mises à jour.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Sky Follower Bridge n'est disponible que pour les navigateurs de bureau. Les navigateurs mobiles ne sont pas supportés.
|
||||
:::
|
||||
|
||||
## Utilisation
|
||||
|
||||
### 1. Naviguez vers 𝕏 (Twitter)
|
||||
|
@ -13,7 +13,7 @@ hero:
|
||||
text: Troubleshooting
|
||||
link: /fr/troubleshooting
|
||||
image:
|
||||
src: /images/logo.png
|
||||
src: /images/logo.webp
|
||||
alt: Image de couverture de Sky Follower Bridge
|
||||
|
||||
features:
|
||||
|
@ -85,6 +85,16 @@ Les permissions du site devraient être comme ci-dessous :
|
||||
|
||||
## Problèmes de scan
|
||||
|
||||
### Le bouton View Detected Users ne fonctionne pas
|
||||
|
||||
Pour une raison quelconque, le bouton View Detected Users peut ne pas fonctionner.
|
||||
|
||||
**Solution :**
|
||||
1. Faites un clic droit sur l'icône de l'extension et sélectionnez "Options"
|
||||
2. La page des résultats sera affichée
|
||||
|
||||
<img src="/images/click-option.png" alt="cliquer sur option" width="500"/>
|
||||
|
||||
### Le scan s'arrête trop tôt
|
||||
|
||||
Le scan s'arrête avant d'atteindre le bas de la page
|
||||
|
@ -4,7 +4,6 @@ Sky Follower Bridge helps you find and follow your 𝕏 (Twitter) connections on
|
||||
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/dfMK07PJeL4?si=SDC7P8basmoOOdjw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
<p style="color: gray; font-size: 0.8em; line-height: 1.4;">This video demonstrates the Edge version. The latest Chrome version and recent Firefox versions may behave differently. A video tutorial for Chrome is coming soon.</p>
|
||||
|
||||
|
||||
## Installation
|
||||
@ -19,6 +18,10 @@ Sky Follower Bridge is available on:
|
||||
We recommend using the Chrome Web Store version as it's always up to date. Other store versions may lag behind in updates.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Sky Follower Bridge is only available on desktop browsers. Mobile browsers are not supported.
|
||||
:::
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Navigate to 𝕏 (Twitter)
|
||||
|
@ -14,7 +14,7 @@ hero:
|
||||
text: Troubleshooting
|
||||
link: /troubleshooting
|
||||
image:
|
||||
src: /images/logo.png
|
||||
src: /images/logo.webp
|
||||
alt: Sky Follower Bridge Cover Image
|
||||
|
||||
features:
|
||||
|
@ -4,7 +4,6 @@ Sky Follower Bridge ti aiuta a trovare e seguire le tue connessioni su 𝕏 (Twi
|
||||
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/dfMK07PJeL4?si=SDC7P8basmoOOdjw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
<p style="color: gray; font-size: 0.8em; line-height: 1.4;">Questo video dimostra la versione Edge. La versione più recente di Chrome e le versioni recenti di Firefox potrebbero comportarsi diversamente. Un tutorial video per Chrome è in arrivo.</p>
|
||||
|
||||
## Installazione
|
||||
|
||||
@ -18,6 +17,10 @@ Sky Follower Bridge è disponibile su:
|
||||
Consigliamo di utilizzare la versione del Chrome Web Store poiché è sempre aggiornata. Le versioni di altri store potrebbero essere in ritardo con gli aggiornamenti.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Sky Follower Bridge è disponibile solo sui browser desktop. I browser mobili non sono supportati.
|
||||
:::
|
||||
|
||||
## Utilizzo
|
||||
|
||||
### 1. Naviga su 𝕏 (Twitter)
|
||||
|
@ -13,7 +13,7 @@ hero:
|
||||
text: Risoluzione dei problemi
|
||||
link: /it/troubleshooting
|
||||
image:
|
||||
src: /images/logo.png
|
||||
src: /images/logo.webp
|
||||
alt: Immagine di copertina di Sky Follower Bridge
|
||||
|
||||
features:
|
||||
|
@ -85,6 +85,16 @@ I permessi del sito dovrebbero essere come di seguito:
|
||||
|
||||
## Problemi di scansione
|
||||
|
||||
### Il pulsante View Detected Users non funziona
|
||||
|
||||
Per qualche motivo, il pulsante View Detected Users potrebbe non funzionare.
|
||||
|
||||
**Soluzione:**
|
||||
1. Fai clic con il tasto destro sull'icona dell'estensione e seleziona "Opzioni"
|
||||
2. Verrà visualizzata la pagina dei risultati
|
||||
|
||||
<img src="/images/click-option.png" alt="clicca su opzione" width="500"/>
|
||||
|
||||
### La scansione si interrompe presto
|
||||
|
||||
La scansione si interrompe prima di raggiungere il fondo della pagina
|
||||
|
@ -4,7 +4,6 @@ Sky Follower Bridgeは、𝕏(Twitter)のあなたのフォローしてい
|
||||
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/dfMK07PJeL4?si=SDC7P8basmoOOdjw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
<p style="color: gray; font-size: 0.8em; line-height: 1.4;">このビデオはEdgeバージョンを示しています。最新のChromeバージョンと最近のFirefoxバージョンでは動作が異なる場合があります。Chromeのビデオチュートリアルは近日公開予定です。</p>
|
||||
|
||||
## インストール
|
||||
|
||||
@ -18,6 +17,10 @@ Sky Follower Bridgeは以下で利用可能です:
|
||||
Chrome Web Storeバージョンを使用することをお勧めします。常に最新の状態です。他のストアバージョンは更新が遅れる場合があります。
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Sky Follower Bridgeはデスクトップブラウザのみで利用可能です。モバイルブラウザはサポートされていません。
|
||||
:::
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 𝕏(Twitter)に移動
|
||||
|
@ -13,7 +13,7 @@ hero:
|
||||
text: トラブルシューティング
|
||||
link: /ja/troubleshooting
|
||||
image:
|
||||
src: /images/logo.png
|
||||
src: /images/logo.webp
|
||||
alt: Sky Follow Bridge cover image
|
||||
|
||||
features:
|
||||
|
@ -89,6 +89,16 @@ Firefoxで公開されているバージョンは、レート制限エラーに
|
||||
|
||||
## スキャンの問題
|
||||
|
||||
### View Detected Users ボタンが機能しない
|
||||
|
||||
何らかの理由で、View Detected Users ボタンが機能しない場合があります。
|
||||
|
||||
**解決策:**
|
||||
1. 拡張機能のアイコンを右クリックし、「オプション」を選択
|
||||
2. 結果ページが表示されます
|
||||
|
||||
<img src="/images/click-option.png" alt="オプションをクリック" width="500"/>
|
||||
|
||||
### スキャンが途中で停止
|
||||
|
||||
スキャンがページの下部に到達する前に停止する
|
||||
|
@ -4,7 +4,6 @@ O Sky Follower Bridge ajuda você a encontrar e seguir suas conexões do 𝕏 (T
|
||||
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/pVqoDv-1uac?si=jKDFFcKQXh61jBdL" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
<p style="color: gray; font-size: 0.8em; line-height: 1.4;">Este vídeo demonstra a versão Edge. A versão mais recente do Chrome e as versões recentes do Firefox podem se comportar de maneira diferente. Um tutorial em vídeo para o Chrome está chegando em breve.</p>
|
||||
|
||||
## Instalação
|
||||
|
||||
@ -18,6 +17,10 @@ O Sky Follower Bridge está disponível em:
|
||||
Recomendamos usar a versão do Chrome Web Store, pois está sempre atualizada. As versões de outras lojas podem estar atrasadas nas atualizações.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Sky Follower Bridge é apenas compatível com navegadores de desktop. Navegadores móveis não são suportados.
|
||||
:::
|
||||
|
||||
## Uso
|
||||
|
||||
### 1. Navegue para 𝕏 (Twitter)
|
||||
|
@ -13,7 +13,7 @@ hero:
|
||||
text: Troubleshooting
|
||||
link: /pt/troubleshooting
|
||||
image:
|
||||
src: /images/logo.png
|
||||
src: /images/logo.webp
|
||||
alt: Imagem de Capa do Sky Follower Bridge
|
||||
|
||||
features:
|
||||
|
@ -85,6 +85,16 @@ As permissões do site devem ser como abaixo:
|
||||
|
||||
## Problemas de Varredura
|
||||
|
||||
### O botão View Detected Users não funciona
|
||||
|
||||
Por algum motivo, o botão View Detected Users pode não funcionar.
|
||||
|
||||
**Solução:**
|
||||
1. Clique com o botão direito no ícone da extensão e selecione "Opções"
|
||||
2. A página de resultados será exibida
|
||||
|
||||
<img src="/images/click-option.png" alt="clicar em opção" width="500"/>
|
||||
|
||||
### Varredura Para Cedo
|
||||
|
||||
A varredura para antes de chegar ao final da página
|
||||
|
BIN
docs/public/images/click-option.png
Normal file
BIN
docs/public/images/click-option.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 484 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 45 KiB |
BIN
docs/public/images/logo.webp
Normal file
BIN
docs/public/images/logo.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
@ -88,6 +88,16 @@ Site permissions should be like below:
|
||||
|
||||
## Scanning Issues
|
||||
|
||||
### View Detected Users button does not work
|
||||
|
||||
Due to some reason, the View Detected Users button may not work.
|
||||
|
||||
**Solution:**
|
||||
1. Right-click the extension icon and select "Options"
|
||||
2. The results page will be displayed
|
||||
|
||||
<img src="/images/click-option.png" alt="click option" width="500"/>
|
||||
|
||||
### Scan Stops Early
|
||||
|
||||
Scanning stops before reaching the bottom of the page
|
||||
|
@ -4,7 +4,6 @@ Sky Follower Bridge 帮助您在 Bluesky 上找到并关注您的 𝕏 (Twitter)
|
||||
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/dfMK07PJeL4?si=SDC7P8basmoOOdjw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
<p style="color: gray; font-size: 0.8em; line-height: 1.4;">此视频演示了 Edge 版本。最新的 Chrome 版本和最近的 Firefox 版本可能会有所不同。Chrome 的视频教程即将推出。</p>
|
||||
|
||||
## 安装
|
||||
|
||||
@ -18,6 +17,10 @@ Sky Follower Bridge 可在以下平台获取:
|
||||
我们推荐使用 Chrome 网上应用店版本,因为它<E4B8BA><E5AE83><EFBFBD>是最新的。其他商店版本可能会延迟更新。
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Sky Follower Bridge 仅适用于桌面浏览器。移动浏览器不支持。
|
||||
:::
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 访问 𝕏 (Twitter)
|
||||
|
@ -13,7 +13,7 @@ hero:
|
||||
text: Troubleshooting
|
||||
link: /zh/troubleshooting
|
||||
image:
|
||||
src: /images/logo.png
|
||||
src: /images/logo.webp
|
||||
alt: Sky Follower Bridge 封面图片
|
||||
|
||||
features:
|
||||
|
7
lefthook.yml
Normal file
7
lefthook.yml
Normal file
@ -0,0 +1,7 @@
|
||||
pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
check:
|
||||
glob: "src/*.{ts,tsx}"
|
||||
run: npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||
staged_fixed: true
|
17309
package-lock.json
generated
17309
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "sky-follower-bridge",
|
||||
"displayName": "Sky Follower Bridge",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.0",
|
||||
"description": "Instantly find and follow the same users from your Twitter follows on Bluesky.",
|
||||
"author": "kawamataryou",
|
||||
"scripts": {
|
||||
@ -26,34 +26,40 @@
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"@plasmohq/messaging": "^0.6.2",
|
||||
"@plasmohq/storage": "^1.12.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"framer-motion": "^11.11.11",
|
||||
"plasmo": "^0.84.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-toastify": "^10.0.6",
|
||||
"ts-pattern": "^5.0.6",
|
||||
"vanjs-core": "^1.2.8",
|
||||
"vitepress": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.3",
|
||||
"@chromatic-com/storybook": "^3.2.2",
|
||||
"@plasmohq/prettier-plugin-sort-imports": "4.0.1",
|
||||
"@storybook/addon-essentials": "^7.6.12",
|
||||
"@storybook/addon-interactions": "^7.6.12",
|
||||
"@storybook/addon-links": "^7.6.12",
|
||||
"@storybook/addon-onboarding": "^1.0.11",
|
||||
"@storybook/blocks": "^7.6.12",
|
||||
"@storybook/react": "^7.6.12",
|
||||
"@storybook/react-vite": "^7.6.12",
|
||||
"@storybook/test": "^7.6.12",
|
||||
"@storybook/addon-essentials": "^8.4.5",
|
||||
"@storybook/addon-interactions": "^8.4.5",
|
||||
"@storybook/addon-links": "^8.4.5",
|
||||
"@storybook/addon-onboarding": "^8.4.5",
|
||||
"@storybook/blocks": "^8.4.5",
|
||||
"@storybook/react": "^8.4.5",
|
||||
"@storybook/react-vite": "^8.4.5",
|
||||
"@storybook/test": "^8.4.5",
|
||||
"@types/chrome": "0.0.260",
|
||||
"@types/node": "20.11.16",
|
||||
"@types/react": "18.2.51",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"daisyui": "^4.6.1",
|
||||
"daisyui": "^4.12.14",
|
||||
"lefthook": "^1.8.4",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-nesting": "^13.0.1",
|
||||
"prettier": "3.2.4",
|
||||
"storybook": "^7.6.12",
|
||||
"storybook": "^8.4.5",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
@ -77,8 +83,5 @@
|
||||
}
|
||||
},
|
||||
"web_accessible_resources": []
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.20.0"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,8 @@
|
||||
*/
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"tailwindcss/nesting": "postcss-nesting",
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ export const getStyle = () => {
|
||||
const App = () => {
|
||||
const {
|
||||
initialize,
|
||||
modalRef,
|
||||
users,
|
||||
loading,
|
||||
stopRetrieveLoop,
|
||||
@ -33,6 +32,12 @@ const App = () => {
|
||||
listName,
|
||||
} = useRetrieveBskyUsers();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
stopRetrieveLoop();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const messageHandler = (
|
||||
message: {
|
||||
@ -44,6 +49,7 @@ const App = () => {
|
||||
if (Object.values(MESSAGE_NAMES).includes(message.name)) {
|
||||
initialize()
|
||||
.then(() => {
|
||||
setIsModalOpen(true);
|
||||
sendResponse({ hasError: false });
|
||||
})
|
||||
.catch((e) => {
|
||||
@ -76,7 +82,7 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal anchorRef={modalRef} onClose={stopRetrieveLoop}>
|
||||
<Modal open={isModalOpen} onClose={closeModal}>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
{loading && (
|
||||
<p className="text-lg font-bold">
|
||||
|
@ -34,7 +34,7 @@ export const isSimilarUser = (
|
||||
) {
|
||||
return {
|
||||
isSimilar: true,
|
||||
type: BSKY_USER_MATCH_TYPE.HANDLE,
|
||||
type: BSKY_USER_MATCH_TYPE.DESCRIPTION,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -75,22 +75,6 @@ export const isSimilarUser = (
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
bskyProfile.description
|
||||
?.toLocaleLowerCase()
|
||||
.includes(`@${lowerCaseNames.accountName}`) &&
|
||||
!["pfp ", "pfp: ", "pfp by "].some((t) =>
|
||||
bskyProfile.description
|
||||
.toLocaleLowerCase()
|
||||
.includes(`${t}@${lowerCaseNames.accountName}`),
|
||||
)
|
||||
) {
|
||||
return {
|
||||
isSimilar: true,
|
||||
type: BSKY_USER_MATCH_TYPE.DESCRIPTION,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isSimilar: false,
|
||||
type: BSKY_USER_MATCH_TYPE.NONE,
|
||||
|
@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import AlertError from "./AlertError";
|
||||
|
||||
const meta: Meta<typeof AlertError> = {
|
||||
title: "CSUI/AlertError",
|
||||
title: "Components/AlertError",
|
||||
component: AlertError,
|
||||
};
|
||||
export default meta;
|
||||
|
@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import AlertSuccess from "./AlertSuccess";
|
||||
|
||||
const meta: Meta<typeof AlertSuccess> = {
|
||||
title: "CSUI/AlertSuccess",
|
||||
title: "Components/AlertSuccess",
|
||||
component: AlertSuccess,
|
||||
};
|
||||
export default meta;
|
||||
|
20
src/lib/components/AsyncButton.stories.tsx
Normal file
20
src/lib/components/AsyncButton.stories.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import AsyncButton from "./AsyncButton";
|
||||
|
||||
const meta = {
|
||||
title: "Components/AsyncButton",
|
||||
component: AsyncButton,
|
||||
} as Meta<typeof AsyncButton>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof AsyncButton>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "Click Me",
|
||||
onClick: async () => {
|
||||
return new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
},
|
||||
},
|
||||
};
|
39
src/lib/components/ConfirmDialog.stories.tsx
Normal file
39
src/lib/components/ConfirmDialog.stories.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import useConfirm, { ConfirmationDialog } from "./ConfirmDialog";
|
||||
|
||||
const meta = {
|
||||
title: "Components/ConfirmDialog",
|
||||
component: ConfirmationDialog,
|
||||
} satisfies Meta<typeof ConfirmationDialog>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
message: "Are you sure you want to proceed?",
|
||||
open: false,
|
||||
handleConfirm: () => {},
|
||||
handleCancel: () => {},
|
||||
},
|
||||
render: (args) => {
|
||||
const { ConfirmationDialog, confirm } = useConfirm({
|
||||
message: args.message,
|
||||
});
|
||||
|
||||
const handleClick = async () => {
|
||||
const result = await confirm();
|
||||
alert(`Confirmed: ${result}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button type="button" onClick={handleClick} className="btn btn-primary">
|
||||
Open Confirm Dialog
|
||||
</button>
|
||||
<ConfirmationDialog />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
97
src/lib/components/ConfirmDialog.tsx
Normal file
97
src/lib/components/ConfirmDialog.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export const ConfirmationDialog = ({
|
||||
title,
|
||||
message,
|
||||
open,
|
||||
handleConfirm,
|
||||
handleCancel,
|
||||
cancelText = "Cancel",
|
||||
okText = "OK",
|
||||
}: {
|
||||
title?: string;
|
||||
message: string;
|
||||
open: boolean;
|
||||
cancelText?: string;
|
||||
okText?: string;
|
||||
handleConfirm: () => void;
|
||||
handleCancel: () => void;
|
||||
}) => (
|
||||
<dialog id="my_modal_1" className="modal" open={open}>
|
||||
<div className="modal-box">
|
||||
{title && <h3 className="font-bold text-xl mb-2">{title}</h3>}
|
||||
<p className="text-sm">{message}</p>
|
||||
<div className="modal-action">
|
||||
<form method="dialog">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-neutral btn-sm min-w-24"
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm min-w-24"
|
||||
type="button"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{okText}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
|
||||
const useConfirm = ({
|
||||
title = "Confirm",
|
||||
message = "Are you sure you want to proceed?",
|
||||
cancelText = "Cancel",
|
||||
okText = "OK",
|
||||
}: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
cancelText?: string;
|
||||
okText?: string;
|
||||
}) => {
|
||||
const [promise, setPromise] = useState(null);
|
||||
|
||||
const confirm = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setPromise({ resolve });
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setPromise(null);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
promise?.resolve(true);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
promise?.resolve(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return {
|
||||
ConfirmationDialog: () => (
|
||||
<ConfirmationDialog
|
||||
title={title}
|
||||
message={message}
|
||||
open={promise !== null}
|
||||
handleConfirm={handleConfirm}
|
||||
handleCancel={handleCancel}
|
||||
cancelText={cancelText}
|
||||
okText={okText}
|
||||
/>
|
||||
),
|
||||
confirm,
|
||||
};
|
||||
};
|
||||
|
||||
export default useConfirm;
|
@ -1,49 +0,0 @@
|
||||
const Header = () => {
|
||||
return (
|
||||
<div className="navbar bg-base-100 border-b border-base-200">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
d="M36 8H13c-3 0-9 2-9 8s6 8 9 8h22c3 0 9 2 9 8s-6 8-9 8H12"
|
||||
/>
|
||||
<path d="M40 12a4 4 0 1 0 0-8a4 4 0 0 0 0 8ZM8 44a4 4 0 1 0 0-8a4 4 0 0 0 0 8Z" />
|
||||
</g>
|
||||
</svg>
|
||||
<span className="text-2xl font-bold">Sky Follower Bridge</span>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<a
|
||||
href="https://github.com/kawamataryo/sky-follower-bridge"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-ghost"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="fill-current"
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
@ -1,23 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { BSKY_USER_MATCH_TYPE } from "../constants";
|
||||
import MatchTypeFilter from "./MatchTypeFilter";
|
||||
|
||||
const meta: Meta<typeof MatchTypeFilter> = {
|
||||
title: "CSUI/MatchTypeFilter",
|
||||
component: MatchTypeFilter,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof MatchTypeFilter>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: {
|
||||
[BSKY_USER_MATCH_TYPE.HANDLE]: true,
|
||||
[BSKY_USER_MATCH_TYPE.DISPLAY_NAME]: false,
|
||||
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: true,
|
||||
},
|
||||
onChange: () => {},
|
||||
},
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
import React from "react";
|
||||
import type { MatchType } from "../../types";
|
||||
import { BSKY_USER_MATCH_TYPE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
|
||||
|
||||
export type MatchTypeFilterValue = {
|
||||
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: boolean;
|
||||
[BSKY_USER_MATCH_TYPE.DISPLAY_NAME]: boolean;
|
||||
[BSKY_USER_MATCH_TYPE.HANDLE]: boolean;
|
||||
};
|
||||
|
||||
export type props = {
|
||||
value: MatchTypeFilterValue;
|
||||
onChange: (key: MatchType) => void;
|
||||
};
|
||||
|
||||
const MatchTypeFilter = ({ value, onChange }: props) => {
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
{Object.keys(value).map((key: MatchType) => (
|
||||
<div className="form-control" key={key}>
|
||||
<label
|
||||
htmlFor={key}
|
||||
className={`badge badge-${
|
||||
MATCH_TYPE_LABEL_AND_COLOR[key].color
|
||||
} gap-1 cursor-pointer py-3 ${value[key] ? "" : "badge-outline"}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={key}
|
||||
checked={value[key]}
|
||||
onChange={() => onChange(key)}
|
||||
className="checkbox checkbox-xs"
|
||||
/>
|
||||
<span className="">{MATCH_TYPE_LABEL_AND_COLOR[key].label}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MatchTypeFilter;
|
@ -1,32 +1,29 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { useRef } from "react";
|
||||
import BlueskyIconSvg from "./Icons/BlueskyIconSvg";
|
||||
import { useState } from "react";
|
||||
import Modal from "./Modal";
|
||||
import UserCard, { type Props as UserCardProps } from "./UserCard";
|
||||
|
||||
const meta: Meta<typeof UserCard> = {
|
||||
title: "CSUI/Modal",
|
||||
component: UserCard,
|
||||
const meta: Meta<typeof Modal> = {
|
||||
title: "Components/Modal",
|
||||
component: Modal,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<{ items: UserCardProps["user"][] }>;
|
||||
type Story = StoryObj<typeof Modal>;
|
||||
|
||||
const DefaultTemplate: Story = {
|
||||
render: () => {
|
||||
const modalRef = useRef<HTMLDialogElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => modalRef.current?.showModal()}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
open
|
||||
</button>
|
||||
<Modal anchorRef={modalRef}>
|
||||
<Modal open={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||
<p>Modal content</p>
|
||||
</Modal>
|
||||
</>
|
||||
@ -34,42 +31,6 @@ const DefaultTemplate: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const ShowModalTemplate: Story = {
|
||||
render: () => {
|
||||
const modalRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => modalRef.current?.showModal()}
|
||||
>
|
||||
open
|
||||
</button>
|
||||
<Modal anchorRef={modalRef} open>
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-xl font-bold">🔎 Find Bluesky Users</h1>
|
||||
<div className="text-xl">34 / 160</div>
|
||||
</div>
|
||||
<div className="flex gap-1 items-center mt-3">
|
||||
<p className="">Match type: </p>
|
||||
<div className="badge badge-info">Same handle name</div>
|
||||
<div className="badge badge-warning">Same display name</div>
|
||||
<div className="badge badge-secondary">
|
||||
Included handle name in description
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
...DefaultTemplate,
|
||||
};
|
||||
|
||||
export const ShowModal = {
|
||||
...ShowModalTemplate,
|
||||
};
|
||||
|
@ -1,14 +1,15 @@
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export type Props = {
|
||||
children: React.ReactNode;
|
||||
anchorRef: React.RefObject<HTMLDialogElement>;
|
||||
open?: boolean;
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const Modal = ({ children, anchorRef, open = false, onClose }: Props) => {
|
||||
const Modal = ({ children, open = false, onClose }: Props) => {
|
||||
const anchorRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (anchorRef.current) {
|
||||
anchorRef.current.addEventListener("close", onClose);
|
||||
@ -19,7 +20,7 @@ const Modal = ({ children, anchorRef, open = false, onClose }: Props) => {
|
||||
anchorRef.current.removeEventListener("close", onClose);
|
||||
}
|
||||
};
|
||||
}, [anchorRef, onClose]);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BSKY_USER_MATCH_TYPE } from "../constants";
|
||||
import { ACTION_MODE, BSKY_USER_MATCH_TYPE } from "../constants";
|
||||
import Sidebar from "./Sidebar";
|
||||
|
||||
const meta = {
|
||||
@ -15,7 +15,7 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
detectedCount: 42,
|
||||
detectedCount: 40,
|
||||
filterValue: {
|
||||
[BSKY_USER_MATCH_TYPE.HANDLE]: true,
|
||||
[BSKY_USER_MATCH_TYPE.DISPLAY_NAME]: false,
|
||||
@ -25,6 +25,16 @@ export const Default: Story = {
|
||||
onChangeFilter: (key) => {
|
||||
console.log(`Filter changed: ${key}`);
|
||||
},
|
||||
matchTypeStats: {
|
||||
[BSKY_USER_MATCH_TYPE.HANDLE]: 10,
|
||||
[BSKY_USER_MATCH_TYPE.DISPLAY_NAME]: 10,
|
||||
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: 10,
|
||||
[BSKY_USER_MATCH_TYPE.FOLLOWING]: 10,
|
||||
},
|
||||
actionAll: async () => {
|
||||
console.log("actionAll");
|
||||
},
|
||||
actionMode: ACTION_MODE.FOLLOW,
|
||||
},
|
||||
};
|
||||
|
||||
@ -40,5 +50,15 @@ export const NoDetections: Story = {
|
||||
onChangeFilter: (key) => {
|
||||
console.log(`Filter changed: ${key}`);
|
||||
},
|
||||
matchTypeStats: {
|
||||
[BSKY_USER_MATCH_TYPE.HANDLE]: 0,
|
||||
[BSKY_USER_MATCH_TYPE.DISPLAY_NAME]: 0,
|
||||
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: 0,
|
||||
[BSKY_USER_MATCH_TYPE.FOLLOWING]: 0,
|
||||
},
|
||||
actionAll: async () => {
|
||||
console.log("actionAll");
|
||||
},
|
||||
actionMode: ACTION_MODE.FOLLOW,
|
||||
},
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ type Props = {
|
||||
onChangeFilter: (key: MatchType) => void;
|
||||
actionAll: () => Promise<void>;
|
||||
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
|
||||
matchTypeStats: Record<MatchType, number>;
|
||||
matchTypeStats: Record<Exclude<MatchType, "none">, number>;
|
||||
};
|
||||
|
||||
const Sidebar = ({
|
||||
@ -179,10 +179,9 @@ const Sidebar = ({
|
||||
className="w-full"
|
||||
>
|
||||
<img
|
||||
height={36}
|
||||
src="https://storage.ko-fi.com/cdn/kofi1.png?v=6"
|
||||
alt="Buy Me a Coffee at ko-fi.com"
|
||||
className="w-[110px] h-auto m-auto"
|
||||
className="w-[120px] h-auto m-auto"
|
||||
/>
|
||||
</a>
|
||||
<div className="divider" />
|
||||
|
@ -1,15 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import Header from "./Header";
|
||||
import SocialLinks from "./SocialLinks";
|
||||
|
||||
const meta = {
|
||||
title: "Header",
|
||||
component: Header,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
} satisfies Meta<typeof Header>;
|
||||
title: "Components/SocialLinks",
|
||||
component: SocialLinks,
|
||||
} as Meta<typeof SocialLinks>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
@ -5,12 +5,12 @@ const SocialLinks = () => {
|
||||
href="https://github.com/kawamataryo/sky-follower-bridge"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="bg-base-100 p-2 rounded-full hover:opacity-80 w-8 h-8"
|
||||
className="bg-base-100 p-2 rounded-full hover:opacity-80"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
@ -21,12 +21,12 @@ const SocialLinks = () => {
|
||||
href="https://bsky.app/profile/kawamataryo.bsky.social"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="bg-base-100 p-2 rounded-full hover:opacity-80 w-8 h-8"
|
||||
className="bg-base-100 p-2 rounded-full hover:opacity-80"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
@ -37,12 +37,12 @@ const SocialLinks = () => {
|
||||
href="https://twitter.com/KawamataRyo"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="bg-base-100 p-2 rounded-full hover:opacity-80 w-8 h-8"
|
||||
className="bg-base-100 p-2 rounded-full hover:opacity-80"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
|
@ -4,7 +4,7 @@ import { ACTION_MODE, BSKY_USER_MATCH_TYPE } from "../constants";
|
||||
import UserCard, { type Props } from "./UserCard";
|
||||
|
||||
const meta: Meta<typeof UserCard> = {
|
||||
title: "CSUI/UserCard",
|
||||
title: "Components/UserCard",
|
||||
component: UserCard,
|
||||
};
|
||||
export default meta;
|
||||
@ -27,13 +27,16 @@ const demoUser: Props["user"] = {
|
||||
Twitter: twitter.com/KawamataRyo
|
||||
GitHub: github.com/kawamataryo
|
||||
Zenn: zenn.dev/ryo_kawamata`,
|
||||
avatar:
|
||||
"https://cdn.bsky.app/img/avatar/plain/did:plc:hcp53er6pefwijpdceo5x4bp/bafkreibm42fe6ionzntt2oryzv2coulgiwh5ejman4vf53bpkdtotszpp4@jpeg",
|
||||
avatar: "https://i.pravatar.cc/150?u=123",
|
||||
matchType: BSKY_USER_MATCH_TYPE.HANDLE,
|
||||
isFollowing: false,
|
||||
followingUri: "",
|
||||
isBlocking: false,
|
||||
blockingUri: "",
|
||||
originalAvatar: "https://i.pravatar.cc/150?u=123",
|
||||
originalHandle: "kawamataryo",
|
||||
originalDisplayName: "KawamataRyo",
|
||||
originalProfileLink: "https://x.com/kawamataryo",
|
||||
};
|
||||
|
||||
const mockAction: Props["clickAction"] = async () => {
|
||||
|
@ -4,6 +4,74 @@ import type { BskyUser } from "~types";
|
||||
import { ACTION_MODE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
|
||||
import AvatarFallbackSvg from "./Icons/AvatarFallbackSvg";
|
||||
|
||||
type UserProfileProps = {
|
||||
avatar: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const UserProfile = ({ avatar, url }: UserProfileProps) => (
|
||||
<div className="avatar">
|
||||
<div className="w-10 h-10 rounded-full border border-white">
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
{avatar ? <img src={avatar} alt="" /> : <AvatarFallbackSvg />}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type UserInfoProps = {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const UserInfo = ({ handle, displayName, url }: UserInfoProps) => (
|
||||
<div>
|
||||
<h2 className="card-title break-all text-[1.1rem] font-bold">
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
{displayName}
|
||||
</a>
|
||||
</h2>
|
||||
<p className="w-fit break-all text-gray-500 dark:text-gray-400 text-sm">
|
||||
<a href={url} target="_blank" rel="noreferrer" className="break-all">
|
||||
@{handle}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
type ActionButtonProps = {
|
||||
loading: boolean;
|
||||
actionBtnLabelAndClass: { label: string; class: string };
|
||||
handleActionButtonClick: () => void;
|
||||
setIsBtnHovered: (value: boolean) => void;
|
||||
setIsJustClicked: (value: boolean) => void;
|
||||
};
|
||||
|
||||
const ActionButton = ({
|
||||
loading,
|
||||
actionBtnLabelAndClass,
|
||||
handleActionButtonClick,
|
||||
setIsBtnHovered,
|
||||
setIsJustClicked,
|
||||
}: ActionButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm rounded-3xl ${
|
||||
loading ? "" : actionBtnLabelAndClass.class
|
||||
}`}
|
||||
onClick={handleActionButtonClick}
|
||||
onMouseEnter={() => setIsBtnHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setIsBtnHovered(false);
|
||||
setIsJustClicked(false);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Processing..." : actionBtnLabelAndClass.label}
|
||||
</button>
|
||||
);
|
||||
|
||||
export type Props = {
|
||||
user: BskyUser;
|
||||
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
|
||||
@ -82,67 +150,64 @@ const UserCard = ({ user, actionMode, clickAction }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-base-100 w-full relative">
|
||||
<div
|
||||
className={`border-l-8 border-${
|
||||
MATCH_TYPE_LABEL_AND_COLOR[user.matchType].color
|
||||
} card-body relative py-3 px-4 rounded-sm grid grid-cols-[70px_1fr]`}
|
||||
>
|
||||
<div>
|
||||
<div className="avatar">
|
||||
<div className="w-14 rounded-full border border-white ">
|
||||
<a
|
||||
href={`https://bsky.app/profile/${user.handle}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{user.avatar ? (
|
||||
<img src={user.avatar} alt="" />
|
||||
) : (
|
||||
<AvatarFallbackSvg />
|
||||
)}
|
||||
</a>
|
||||
<div className="bg-base-100 w-full relative grid grid-cols-[22%_1fr] gap-5">
|
||||
<div className="flex flex-row gap-2 bg-slate-100 dark:bg-slate-800 justify-between pr-2">
|
||||
<div
|
||||
className={`border-l-8 border-${
|
||||
MATCH_TYPE_LABEL_AND_COLOR[user.matchType].color
|
||||
} relative py-3 pl-4 pr-1 grid grid-cols-[50px_1fr]`}
|
||||
>
|
||||
<UserProfile
|
||||
avatar={user.originalAvatar}
|
||||
url={user.originalProfileLink}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<UserInfo
|
||||
handle={user.originalHandle}
|
||||
displayName={user.originalDisplayName}
|
||||
url={user.originalProfileLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative py-3 pl-0 pr-2 grid grid-cols-[50px_1fr]">
|
||||
<UserProfile
|
||||
avatar={user.avatar}
|
||||
url={`https://bsky.app/profile/${user.handle}`}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div>
|
||||
<h2 className="card-title break-all">
|
||||
<a
|
||||
href={`https://bsky.app/profile/${user.handle}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{user.displayName}
|
||||
</a>
|
||||
</h2>
|
||||
<p className="whitespace-nowrap w-fit break-all text-gray-500 dark:text-gray-400 text-sm">
|
||||
<a
|
||||
href={`https://bsky.app/profile/${user.handle}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@{user.handle}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<UserInfo
|
||||
handle={user.handle}
|
||||
displayName={user.displayName}
|
||||
url={`https://bsky.app/profile/${user.handle}`}
|
||||
/>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm rounded-3xl ${
|
||||
loading ? "" : actionBtnLabelAndClass.class
|
||||
}`}
|
||||
onClick={handleActionButtonClick}
|
||||
onMouseEnter={() => setIsBtnHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setIsBtnHovered(false);
|
||||
setIsJustClicked(false);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Processing..." : actionBtnLabelAndClass.label}
|
||||
</button>
|
||||
<ActionButton
|
||||
loading={loading}
|
||||
actionBtnLabelAndClass={actionBtnLabelAndClass}
|
||||
handleActionButtonClick={handleActionButtonClick}
|
||||
setIsBtnHovered={setIsBtnHovered}
|
||||
setIsJustClicked={setIsJustClicked}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm break-all">{user.description}</p>
|
||||
|
@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import UserCardSkeleton from "./UserCardSkeleton";
|
||||
|
||||
const meta: Meta<typeof UserCardSkeleton> = {
|
||||
title: "CSUI/UserCardSkeleton",
|
||||
title: "Components/UserCardSkeleton",
|
||||
component: UserCardSkeleton,
|
||||
};
|
||||
export default meta;
|
||||
|
@ -2,11 +2,28 @@ import type { AtpSessionData } from "@atproto/api";
|
||||
import { Storage } from "@plasmohq/storage";
|
||||
import { useStorage } from "@plasmohq/storage/hook";
|
||||
import React from "react";
|
||||
import { P, match } from "ts-pattern";
|
||||
import { BskyServiceWorkerClient } from "~lib/bskyServiceWorkerClient";
|
||||
import { type MESSAGE_NAMES, STORAGE_KEYS } from "~lib/constants";
|
||||
import { MESSAGE_NAMES, STORAGE_KEYS } from "~lib/constants";
|
||||
import { searchBskyUser } from "~lib/searchBskyUsers";
|
||||
import { XService } from "~lib/services/x";
|
||||
import type { BskyUser, CrawledUserInfo } from "~types";
|
||||
import type { AbstractService } from "~lib/services/abstractService";
|
||||
import { XService } from "~lib/services/xService";
|
||||
import type { BskyUser, CrawledUserInfo, MessageName } from "~types";
|
||||
|
||||
const getService = (messageName: string): AbstractService => {
|
||||
return match(messageName)
|
||||
.with(
|
||||
P.when((name) =>
|
||||
[
|
||||
MESSAGE_NAMES.SEARCH_BSKY_USER_ON_FOLLOW_PAGE,
|
||||
MESSAGE_NAMES.SEARCH_BSKY_USER_ON_LIST_MEMBERS_PAGE,
|
||||
MESSAGE_NAMES.SEARCH_BSKY_USER_ON_BLOCK_PAGE,
|
||||
].includes(name as MessageName),
|
||||
),
|
||||
() => new XService(messageName),
|
||||
)
|
||||
.otherwise(() => new XService(messageName));
|
||||
};
|
||||
|
||||
const scrapeListNameFromPage = (): string => {
|
||||
const listNameElement = document.querySelector(
|
||||
@ -39,11 +56,6 @@ export const useRetrieveBskyUsers = () => {
|
||||
}>(null);
|
||||
const [listName, setListName] = React.useState<string>("");
|
||||
|
||||
const modalRef = React.useRef<HTMLDialogElement>(null);
|
||||
const showModal = () => {
|
||||
modalRef.current?.showModal();
|
||||
};
|
||||
|
||||
const retrieveBskyUsers = React.useCallback(
|
||||
async (usersData: CrawledUserInfo[]) => {
|
||||
for (const userData of usersData) {
|
||||
@ -69,6 +81,10 @@ export const useRetrieveBskyUsers = () => {
|
||||
followingUri: searchResult.bskyProfile.viewer?.following,
|
||||
isBlocking: !!searchResult.bskyProfile.viewer?.blocking,
|
||||
blockingUri: searchResult.bskyProfile.viewer?.blocking,
|
||||
originalAvatar: userData.originalAvatar,
|
||||
originalHandle: userData.accountName,
|
||||
originalDisplayName: userData.displayName,
|
||||
originalProfileLink: userData.originalProfileLink,
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -86,7 +102,7 @@ export const useRetrieveBskyUsers = () => {
|
||||
|
||||
let index = 0;
|
||||
|
||||
const xService = new XService(messageName);
|
||||
const service = getService(messageName);
|
||||
|
||||
// loop until we get to the bottom
|
||||
while (!isBottomReached) {
|
||||
@ -94,10 +110,10 @@ export const useRetrieveBskyUsers = () => {
|
||||
break;
|
||||
}
|
||||
|
||||
const data = xService.getCrawledUsers();
|
||||
const data = service.getCrawledUsers();
|
||||
await retrieveBskyUsers(data);
|
||||
|
||||
const isEnd = await xService.performScrollAndCheckEnd();
|
||||
const isEnd = await service.performScrollAndCheckEnd();
|
||||
|
||||
if (isEnd) {
|
||||
setIsBottomReached(true);
|
||||
@ -155,7 +171,6 @@ export const useRetrieveBskyUsers = () => {
|
||||
});
|
||||
setLoading(true);
|
||||
await setUsers([]);
|
||||
showModal();
|
||||
}, []);
|
||||
|
||||
const restart = React.useCallback(() => {
|
||||
@ -177,8 +192,6 @@ export const useRetrieveBskyUsers = () => {
|
||||
);
|
||||
|
||||
return {
|
||||
modalRef,
|
||||
showModal,
|
||||
initialize,
|
||||
users,
|
||||
listName,
|
||||
|
36
src/lib/services/abstractService.ts
Normal file
36
src/lib/services/abstractService.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { MESSAGE_NAME_TO_QUERY_PARAM_MAP } from "~lib/constants";
|
||||
import type { CrawledUserInfo, MessageName } from "~types";
|
||||
|
||||
export abstract class AbstractService {
|
||||
messageName: MessageName;
|
||||
crawledUsers: Set<string>;
|
||||
|
||||
constructor(messageName: string) {
|
||||
this.messageName = messageName as MessageName;
|
||||
this.crawledUsers = new Set();
|
||||
}
|
||||
|
||||
abstract extractUserData(userCell: Element): CrawledUserInfo;
|
||||
|
||||
getCrawledUsers(): CrawledUserInfo[] {
|
||||
const userCells = Array.from(
|
||||
document.querySelectorAll(
|
||||
MESSAGE_NAME_TO_QUERY_PARAM_MAP[this.messageName],
|
||||
),
|
||||
);
|
||||
|
||||
const users = Array.from(userCells)
|
||||
.map((userCell) => this.extractUserData(userCell))
|
||||
.filter((user) => {
|
||||
const isNewUser = !this.crawledUsers.has(user.accountName);
|
||||
if (isNewUser) {
|
||||
this.crawledUsers.add(user.accountName);
|
||||
}
|
||||
return isNewUser;
|
||||
});
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
abstract performScrollAndCheckEnd(): Promise<boolean>;
|
||||
}
|
@ -1,20 +1,11 @@
|
||||
import { MESSAGE_NAMES } from "~lib/constants";
|
||||
import { BSKY_DOMAIN, MESSAGE_NAME_TO_QUERY_PARAM_MAP } from "~lib/constants";
|
||||
import { wait } from "~lib/utils";
|
||||
import type { CrawledUserInfo, MessageName } from "~types";
|
||||
import type { CrawledUserInfo } from "~types";
|
||||
import { AbstractService } from "./abstractService";
|
||||
|
||||
export class XService {
|
||||
// 対象のdomを取得する処理
|
||||
messageName: MessageName;
|
||||
crawledUsers: Set<string>;
|
||||
|
||||
constructor(messageName: string) {
|
||||
// TODO: add type check
|
||||
this.messageName = messageName as MessageName;
|
||||
this.crawledUsers = new Set();
|
||||
}
|
||||
|
||||
private extractUserData(userCell: Element): CrawledUserInfo {
|
||||
export class XService extends AbstractService {
|
||||
extractUserData(userCell: Element): CrawledUserInfo {
|
||||
const anchors = Array.from(userCell.querySelectorAll("a"));
|
||||
const [avatarEl, displayNameEl] = anchors;
|
||||
const accountName = avatarEl?.getAttribute("href")?.replace("/", "");
|
||||
@ -29,6 +20,10 @@ export class XService {
|
||||
?.match(/bsky\.app\/profile\/([^/\s]+)…?/)?.[1]
|
||||
?.replace("…", "") ??
|
||||
"";
|
||||
const originalAvatar = userCell
|
||||
.querySelector('[data-testid^="UserAvatar-Container"]')
|
||||
?.querySelector("img")
|
||||
?.getAttribute("src");
|
||||
|
||||
return {
|
||||
accountName,
|
||||
@ -36,28 +31,11 @@ export class XService {
|
||||
accountNameRemoveUnderscore,
|
||||
accountNameReplaceUnderscore,
|
||||
bskyHandle,
|
||||
originalAvatar,
|
||||
originalProfileLink: `https://x.com/${accountName}`,
|
||||
};
|
||||
}
|
||||
|
||||
getCrawledUsers(): CrawledUserInfo[] {
|
||||
const userCells = Array.from(
|
||||
document.querySelectorAll(
|
||||
MESSAGE_NAME_TO_QUERY_PARAM_MAP[this.messageName],
|
||||
),
|
||||
);
|
||||
|
||||
const users = userCells
|
||||
.map((userCell) => this.extractUserData(userCell))
|
||||
.filter((user) => !this.crawledUsers.has(user.accountName));
|
||||
|
||||
this.crawledUsers = new Set([
|
||||
...this.crawledUsers,
|
||||
...users.map((user) => user.accountName),
|
||||
]);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
async performScrollAndCheckEnd(): Promise<boolean> {
|
||||
const isListMembersPage =
|
||||
this.messageName === MESSAGE_NAMES.SEARCH_BSKY_USER_ON_LIST_MEMBERS_PAGE;
|
@ -1,7 +1,10 @@
|
||||
import UserCard from "~lib/components/UserCard";
|
||||
import { useBskyUserManager } from "~lib/hooks/useBskyUserManager";
|
||||
import "./style.css";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import useConfirm from "~lib/components/ConfirmDialog";
|
||||
import Sidebar from "~lib/components/Sidebar";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
const Option = () => {
|
||||
const {
|
||||
@ -16,17 +19,23 @@ const Option = () => {
|
||||
matchTypeStats,
|
||||
} = useBskyUserManager();
|
||||
|
||||
const { confirm, ConfirmationDialog } = useConfirm({
|
||||
title: "Proceed with Execution?",
|
||||
message:
|
||||
"User detection is not perfect and may include false positives. Do you still want to proceed?",
|
||||
cancelText: "Cancel",
|
||||
okText: "OK",
|
||||
});
|
||||
|
||||
const handleActionAll = async () => {
|
||||
if (
|
||||
!window.confirm(
|
||||
"User detection is not perfect and may include false positives. Do you still want to proceed?",
|
||||
)
|
||||
) {
|
||||
if (!(await confirm())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await actionAll();
|
||||
const result = await actionAll();
|
||||
toast.success(`Followed ${result} users`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen">
|
||||
@ -40,22 +49,30 @@ const Option = () => {
|
||||
matchTypeStats={matchTypeStats}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 ml-80 p-6 overflow-y-auto">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="divide-y divide-gray-500">
|
||||
{filteredUsers.map((user) => (
|
||||
<UserCard
|
||||
key={user.handle}
|
||||
user={user}
|
||||
clickAction={handleClickAction}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 ml-80 p-6 pt-0 overflow-y-auto">
|
||||
<div className="grid grid-cols-[22%_1fr] sticky top-0 z-10 bg-base-100 border-b-[1px] border-gray-500">
|
||||
<h2 className="text-lg font-bold text-center py-2">Source</h2>
|
||||
<h2 className="text-lg font-bold text-center py-2">Detected</h2>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="divide-y divide-gray-500">
|
||||
{filteredUsers.map((user) => (
|
||||
<UserCard
|
||||
key={user.handle}
|
||||
user={user}
|
||||
clickAction={handleClickAction}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={5000}
|
||||
className="text-sm"
|
||||
/>
|
||||
<ConfirmationDialog />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { type FormEvent, useCallback, useEffect, useState } from "react";
|
||||
import { P, match } from "ts-pattern";
|
||||
import packageJson from "../package.json";
|
||||
|
||||
import "./style.css";
|
||||
|
||||
@ -213,7 +214,8 @@ function IndexPopup() {
|
||||
<path d="M40 12a4 4 0 1 0 0-8a4 4 0 0 0 0 8ZM8 44a4 4 0 1 0 0-8a4 4 0 0 0 0 8Z" />
|
||||
</g>
|
||||
</svg>
|
||||
Sky Follower Bridge
|
||||
Sky Follower Bridge{" "}
|
||||
<span className="text-sm self-end">v{packageJson.version}</span>
|
||||
</h1>
|
||||
<form onSubmit={searchBskyUser} className="mt-5">
|
||||
<label className="w-full block" htmlFor="identifier">
|
||||
|
@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.modal {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.modal {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
@ -16,6 +16,10 @@ export type BskyUser = {
|
||||
followingUri: string | null;
|
||||
isBlocking: boolean;
|
||||
blockingUri: string | null;
|
||||
originalAvatar: string;
|
||||
originalHandle: string;
|
||||
originalDisplayName: string;
|
||||
originalProfileLink: string;
|
||||
};
|
||||
|
||||
export type MatchTypeFilterValue = {
|
||||
@ -31,4 +35,6 @@ export type CrawledUserInfo = {
|
||||
accountNameRemoveUnderscore: string;
|
||||
accountNameReplaceUnderscore: string;
|
||||
bskyHandle: string;
|
||||
originalAvatar: string;
|
||||
originalProfileLink: string;
|
||||
};
|
||||
|
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user