mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
feat: enhance settings view with integration options for start.gg and Challonge, add manual token dialogs, and improve keyboard shortcut management
This commit is contained in:
@@ -24,6 +24,14 @@ type Translations = {
|
|||||||
settingsShortcutRightDecrementHint: string;
|
settingsShortcutRightDecrementHint: string;
|
||||||
settingsShortcutReset: string;
|
settingsShortcutReset: string;
|
||||||
settingsShortcutRecordingHint: string;
|
settingsShortcutRecordingHint: string;
|
||||||
|
settingsShortcutConflictWarning: string;
|
||||||
|
settingsShortcutStartRecording: string;
|
||||||
|
settingsShortcutStopRecording: string;
|
||||||
|
settingsShortcutResetSingle: string;
|
||||||
|
settingsIntegrationsTitle: string;
|
||||||
|
settingsIntegrationsDescription: string;
|
||||||
|
settingsDisconnect: string;
|
||||||
|
settingsNotConnected: string;
|
||||||
languageEnglish: string;
|
languageEnglish: string;
|
||||||
languageSpanish: string;
|
languageSpanish: string;
|
||||||
scoreboardUnassigned: string;
|
scoreboardUnassigned: string;
|
||||||
@@ -53,6 +61,8 @@ type Translations = {
|
|||||||
aboutElectronNote: string;
|
aboutElectronNote: string;
|
||||||
aboutUnknownReleaseError: string;
|
aboutUnknownReleaseError: string;
|
||||||
aboutGitHubStatusError: string;
|
aboutGitHubStatusError: string;
|
||||||
|
aboutChangelog: string;
|
||||||
|
aboutTechStackTitle: string;
|
||||||
graphicsTitle: string;
|
graphicsTitle: string;
|
||||||
graphicsDescription: string;
|
graphicsDescription: string;
|
||||||
graphicsNoConfigured: string;
|
graphicsNoConfigured: string;
|
||||||
@@ -61,10 +71,16 @@ type Translations = {
|
|||||||
graphicsScoreboard: string;
|
graphicsScoreboard: string;
|
||||||
graphicsCommentary: string;
|
graphicsCommentary: string;
|
||||||
graphicsSkinLabel: string;
|
graphicsSkinLabel: string;
|
||||||
|
graphicsCopied: string;
|
||||||
|
graphicsOpenBrowser: string;
|
||||||
commentaryTitle: string;
|
commentaryTitle: string;
|
||||||
commentaryCommentator1: string;
|
commentaryCommentator1: string;
|
||||||
commentaryCommentator2: string;
|
commentaryCommentator2: string;
|
||||||
commentaryTwitterText: string;
|
commentaryTwitterText: string;
|
||||||
|
commentaryTwitterMaxLength: string;
|
||||||
|
commentaryTwitterInvalidChars: string;
|
||||||
|
commentarySwap: string;
|
||||||
|
commentaryClear: string;
|
||||||
bracketTitle: string;
|
bracketTitle: string;
|
||||||
bracketStage: string;
|
bracketStage: string;
|
||||||
bracketSide: string;
|
bracketSide: string;
|
||||||
@@ -85,18 +101,8 @@ type Translations = {
|
|||||||
playersSearchPlaceholder: string;
|
playersSearchPlaceholder: string;
|
||||||
playersImport: string;
|
playersImport: string;
|
||||||
playersExport: string;
|
playersExport: string;
|
||||||
commentaryTwitterMaxLength: string;
|
playersConnectInSettings: string;
|
||||||
commentaryTwitterInvalidChars: string;
|
playersConnectInSettingsSuffix: string;
|
||||||
commentarySwap: string;
|
|
||||||
commentaryClear: string;
|
|
||||||
aboutChangelog : string;
|
|
||||||
aboutTechStackTitle : string;
|
|
||||||
settingsShortcutConflictWarning : string;
|
|
||||||
settingsShortcutStartRecording: string;
|
|
||||||
settingsShortcutStopRecording: string;
|
|
||||||
settingsShortcutResetSingle: string;
|
|
||||||
graphicsCopied : string;
|
|
||||||
graphicsOpenBrowser : string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = 'scoreko-dev.language';
|
const STORAGE_KEY = 'scoreko-dev.language';
|
||||||
@@ -108,6 +114,8 @@ const messages: Record<Locale, Translations> = {
|
|||||||
menuGraphics: 'Graphics',
|
menuGraphics: 'Graphics',
|
||||||
menuSettings: 'Settings',
|
menuSettings: 'Settings',
|
||||||
menuAbout: 'About',
|
menuAbout: 'About',
|
||||||
|
|
||||||
|
// ── Settings ────────────────────────────────────────────────────────────
|
||||||
settingsTitle: 'Settings',
|
settingsTitle: 'Settings',
|
||||||
settingsDescription: 'Dashboard and bundle configuration.',
|
settingsDescription: 'Dashboard and bundle configuration.',
|
||||||
settingsLanguageLabel: 'Language',
|
settingsLanguageLabel: 'Language',
|
||||||
@@ -124,8 +132,20 @@ const messages: Record<Locale, Translations> = {
|
|||||||
settingsShortcutRightDecrementHint: 'Decreases right player score by one.',
|
settingsShortcutRightDecrementHint: 'Decreases right player score by one.',
|
||||||
settingsShortcutReset: 'Reset shortcuts',
|
settingsShortcutReset: 'Reset shortcuts',
|
||||||
settingsShortcutRecordingHint: 'Press the desired shortcut now (example: Alt+1).',
|
settingsShortcutRecordingHint: 'Press the desired shortcut now (example: Alt+1).',
|
||||||
|
settingsShortcutConflictWarning: 'This shortcut is already assigned to another action.',
|
||||||
|
settingsShortcutStartRecording: 'Start recording shortcut',
|
||||||
|
settingsShortcutStopRecording: 'Stop recording shortcut',
|
||||||
|
settingsShortcutResetSingle: 'Reset this shortcut',
|
||||||
|
settingsIntegrationsTitle: 'Integrations',
|
||||||
|
settingsIntegrationsDescription: 'Connect your tournament platform accounts to import players directly from brackets.',
|
||||||
|
settingsDisconnect: 'Disconnect',
|
||||||
|
settingsNotConnected: 'Not connected',
|
||||||
|
|
||||||
|
// ── Language ─────────────────────────────────────────────────────────────
|
||||||
languageEnglish: 'English',
|
languageEnglish: 'English',
|
||||||
languageSpanish: 'Spanish',
|
languageSpanish: 'Spanish',
|
||||||
|
|
||||||
|
// ── Scoreboard ───────────────────────────────────────────────────────────
|
||||||
scoreboardUnassigned: '(Unassigned)',
|
scoreboardUnassigned: '(Unassigned)',
|
||||||
scoreboardLeft: 'Left',
|
scoreboardLeft: 'Left',
|
||||||
scoreboardRight: 'Right',
|
scoreboardRight: 'Right',
|
||||||
@@ -137,6 +157,8 @@ const messages: Record<Locale, Translations> = {
|
|||||||
scoreboardLabelTeam: 'Team',
|
scoreboardLabelTeam: 'Team',
|
||||||
scoreboardLabelCountry: 'Country',
|
scoreboardLabelCountry: 'Country',
|
||||||
scoreboardLabelGame: 'Game',
|
scoreboardLabelGame: 'Game',
|
||||||
|
|
||||||
|
// ── About ────────────────────────────────────────────────────────────────
|
||||||
aboutTitle: 'About',
|
aboutTitle: 'About',
|
||||||
aboutVersion: 'Version',
|
aboutVersion: 'Version',
|
||||||
aboutDescription: 'Dashboard for producing fighting game overlays using NodeCG, Vue, and Quasar.',
|
aboutDescription: 'Dashboard for producing fighting game overlays using NodeCG, Vue, and Quasar.',
|
||||||
@@ -153,6 +175,10 @@ const messages: Record<Locale, Translations> = {
|
|||||||
aboutElectronNote: 'Note for Electron: this panel only implements detection and notification. For real automatic desktop updates, you need to integrate autoUpdater into Electron\'s main process and publish signed artifacts per platform.',
|
aboutElectronNote: 'Note for Electron: this panel only implements detection and notification. For real automatic desktop updates, you need to integrate autoUpdater into Electron\'s main process and publish signed artifacts per platform.',
|
||||||
aboutUnknownReleaseError: 'Unknown error while checking releases.',
|
aboutUnknownReleaseError: 'Unknown error while checking releases.',
|
||||||
aboutGitHubStatusError: 'GitHub responded with status',
|
aboutGitHubStatusError: 'GitHub responded with status',
|
||||||
|
aboutChangelog: 'Changelog',
|
||||||
|
aboutTechStackTitle: 'Tech stack',
|
||||||
|
|
||||||
|
// ── Graphics ─────────────────────────────────────────────────────────────
|
||||||
graphicsTitle: 'Graphics',
|
graphicsTitle: 'Graphics',
|
||||||
graphicsDescription: 'Bundle graphics controls and status.',
|
graphicsDescription: 'Bundle graphics controls and status.',
|
||||||
graphicsNoConfigured: 'There are no graphics configured in this bundle.',
|
graphicsNoConfigured: 'There are no graphics configured in this bundle.',
|
||||||
@@ -161,19 +187,31 @@ const messages: Record<Locale, Translations> = {
|
|||||||
graphicsScoreboard: 'Scoreboard',
|
graphicsScoreboard: 'Scoreboard',
|
||||||
graphicsCommentary: 'Commentary',
|
graphicsCommentary: 'Commentary',
|
||||||
graphicsSkinLabel: 'Skin',
|
graphicsSkinLabel: 'Skin',
|
||||||
|
graphicsCopied: 'URL copied to clipboard',
|
||||||
|
graphicsOpenBrowser: 'Open in browser',
|
||||||
|
|
||||||
|
// ── Commentary ───────────────────────────────────────────────────────────
|
||||||
commentaryTitle: 'Commentary',
|
commentaryTitle: 'Commentary',
|
||||||
commentaryCommentator1: 'Commentator #1',
|
commentaryCommentator1: 'Commentator #1',
|
||||||
commentaryCommentator2: 'Commentator #2',
|
commentaryCommentator2: 'Commentator #2',
|
||||||
commentaryTwitterText: '@Twitter / Text',
|
commentaryTwitterText: '@Twitter / Text',
|
||||||
|
commentaryTwitterMaxLength: 'Twitter character limit exceeded',
|
||||||
|
commentaryTwitterInvalidChars: 'Invalid characters in Twitter text',
|
||||||
|
commentarySwap: 'Swap commentators',
|
||||||
|
commentaryClear: 'Clear commentary',
|
||||||
|
|
||||||
|
// ── Bracket ──────────────────────────────────────────────────────────────
|
||||||
bracketTitle: 'Bracket',
|
bracketTitle: 'Bracket',
|
||||||
bracketStage: 'Stage',
|
bracketStage: 'Stage',
|
||||||
bracketSide: 'Bracket side',
|
bracketSide: 'Bracket side',
|
||||||
bracketCustomProgress: 'Custom progress',
|
bracketCustomProgress: 'Custom progress',
|
||||||
bracketPreview: 'Preview',
|
bracketPreview: 'Preview',
|
||||||
|
|
||||||
|
// ── Players ──────────────────────────────────────────────────────────────
|
||||||
playersLabelTeam: 'Team',
|
playersLabelTeam: 'Team',
|
||||||
playersLabelCountry: 'Country',
|
playersLabelCountry: 'Country',
|
||||||
playersLabelActions: 'Actions',
|
playersLabelActions: 'Actions',
|
||||||
playersStartggHelp: 'Connect via OAuth (recommended) or paste your personal token to load tournaments you created or administrate. If you see "Client authentication failed", verify your config uses the Client ID/Secret from a start.gg OAuth App.',
|
playersStartggHelp: 'Connect via OAuth (recommended) or paste your personal token to load tournaments you created or administrate.',
|
||||||
playersConnectStartgg: 'Connect with start.gg',
|
playersConnectStartgg: 'Connect with start.gg',
|
||||||
playersConnected: 'Connected',
|
playersConnected: 'Connected',
|
||||||
playersUsePersonalApi: 'Use personal API',
|
playersUsePersonalApi: 'Use personal API',
|
||||||
@@ -185,25 +223,18 @@ const messages: Record<Locale, Translations> = {
|
|||||||
playersSearchPlaceholder: 'Search...',
|
playersSearchPlaceholder: 'Search...',
|
||||||
playersImport: 'Import',
|
playersImport: 'Import',
|
||||||
playersExport: 'Export',
|
playersExport: 'Export',
|
||||||
commentaryTwitterMaxLength: 'Twitter character limit exceeded',
|
playersConnectInSettings: 'Connect your account in',
|
||||||
commentaryTwitterInvalidChars: 'Invalid characters in Twitter text',
|
playersConnectInSettingsSuffix: 'to import players from tournaments.',
|
||||||
commentarySwap: 'Swap commentators',
|
|
||||||
commentaryClear: 'Clear commentary',
|
|
||||||
aboutChangelog: 'Changelog',
|
|
||||||
aboutTechStackTitle: 'Tech stack',
|
|
||||||
settingsShortcutConflictWarning: 'This shortcut is already assigned to',
|
|
||||||
settingsShortcutStartRecording: 'Start recording shortcut',
|
|
||||||
settingsShortcutStopRecording: 'Stop recording shortcut',
|
|
||||||
settingsShortcutResetSingle: 'Reset single player score shortcut',
|
|
||||||
graphicsCopied: 'URL copied to clipboard',
|
|
||||||
graphicsOpenBrowser: 'Open in browser',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
es: {
|
es: {
|
||||||
menuDashboard: 'Panel',
|
menuDashboard: 'Panel',
|
||||||
menuPlayers: 'Jugadores',
|
menuPlayers: 'Jugadores',
|
||||||
menuGraphics: 'Gráficos',
|
menuGraphics: 'Gráficos',
|
||||||
menuSettings: 'Configuración',
|
menuSettings: 'Configuración',
|
||||||
menuAbout: 'Acerca de',
|
menuAbout: 'Acerca de',
|
||||||
|
|
||||||
|
// ── Settings ────────────────────────────────────────────────────────────
|
||||||
settingsTitle: 'Configuración',
|
settingsTitle: 'Configuración',
|
||||||
settingsDescription: 'Configuración del dashboard y del bundle.',
|
settingsDescription: 'Configuración del dashboard y del bundle.',
|
||||||
settingsLanguageLabel: 'Idioma',
|
settingsLanguageLabel: 'Idioma',
|
||||||
@@ -220,8 +251,20 @@ const messages: Record<Locale, Translations> = {
|
|||||||
settingsShortcutRightDecrementHint: 'Reduce en uno el score del jugador derecho.',
|
settingsShortcutRightDecrementHint: 'Reduce en uno el score del jugador derecho.',
|
||||||
settingsShortcutReset: 'Restablecer atajos',
|
settingsShortcutReset: 'Restablecer atajos',
|
||||||
settingsShortcutRecordingHint: 'Pulsa ahora el atajo deseado (ejemplo: Alt+1).',
|
settingsShortcutRecordingHint: 'Pulsa ahora el atajo deseado (ejemplo: Alt+1).',
|
||||||
|
settingsShortcutConflictWarning: 'Este atajo ya está asignado a otra acción.',
|
||||||
|
settingsShortcutStartRecording: 'Iniciar grabación de atajo',
|
||||||
|
settingsShortcutStopRecording: 'Detener grabación de atajo',
|
||||||
|
settingsShortcutResetSingle: 'Restablecer este atajo',
|
||||||
|
settingsIntegrationsTitle: 'Integraciones',
|
||||||
|
settingsIntegrationsDescription: 'Conecta tus cuentas de plataformas de torneos para importar jugadores directamente desde los brackets.',
|
||||||
|
settingsDisconnect: 'Desconectar',
|
||||||
|
settingsNotConnected: 'No conectado',
|
||||||
|
|
||||||
|
// ── Language ─────────────────────────────────────────────────────────────
|
||||||
languageEnglish: 'Inglés',
|
languageEnglish: 'Inglés',
|
||||||
languageSpanish: 'Castellano',
|
languageSpanish: 'Castellano',
|
||||||
|
|
||||||
|
// ── Scoreboard ───────────────────────────────────────────────────────────
|
||||||
scoreboardUnassigned: '(Sin asignar)',
|
scoreboardUnassigned: '(Sin asignar)',
|
||||||
scoreboardLeft: 'Izquierda',
|
scoreboardLeft: 'Izquierda',
|
||||||
scoreboardRight: 'Derecha',
|
scoreboardRight: 'Derecha',
|
||||||
@@ -233,6 +276,8 @@ const messages: Record<Locale, Translations> = {
|
|||||||
scoreboardLabelTeam: 'Equipo',
|
scoreboardLabelTeam: 'Equipo',
|
||||||
scoreboardLabelCountry: 'País',
|
scoreboardLabelCountry: 'País',
|
||||||
scoreboardLabelGame: 'Juego',
|
scoreboardLabelGame: 'Juego',
|
||||||
|
|
||||||
|
// ── About ────────────────────────────────────────────────────────────────
|
||||||
aboutTitle: 'Acerca de',
|
aboutTitle: 'Acerca de',
|
||||||
aboutVersion: 'Versión',
|
aboutVersion: 'Versión',
|
||||||
aboutDescription: 'Dashboard para producir overlays de juegos de lucha usando NodeCG, Vue y Quasar.',
|
aboutDescription: 'Dashboard para producir overlays de juegos de lucha usando NodeCG, Vue y Quasar.',
|
||||||
@@ -249,6 +294,10 @@ const messages: Record<Locale, Translations> = {
|
|||||||
aboutElectronNote: 'Nota para Electron: este panel solo implementa detección y notificación. Para actualizaciones automáticas reales de escritorio, debes integrar autoUpdater en el proceso principal de Electron y publicar artefactos firmados por plataforma.',
|
aboutElectronNote: 'Nota para Electron: este panel solo implementa detección y notificación. Para actualizaciones automáticas reales de escritorio, debes integrar autoUpdater en el proceso principal de Electron y publicar artefactos firmados por plataforma.',
|
||||||
aboutUnknownReleaseError: 'Error desconocido al consultar releases.',
|
aboutUnknownReleaseError: 'Error desconocido al consultar releases.',
|
||||||
aboutGitHubStatusError: 'GitHub respondió con estado',
|
aboutGitHubStatusError: 'GitHub respondió con estado',
|
||||||
|
aboutChangelog: 'Changelog',
|
||||||
|
aboutTechStackTitle: 'Tech stack',
|
||||||
|
|
||||||
|
// ── Graphics ─────────────────────────────────────────────────────────────
|
||||||
graphicsTitle: 'Gráficos',
|
graphicsTitle: 'Gráficos',
|
||||||
graphicsDescription: 'Controles y estado de los gráficos del bundle.',
|
graphicsDescription: 'Controles y estado de los gráficos del bundle.',
|
||||||
graphicsNoConfigured: 'No hay gráficos configurados en este bundle.',
|
graphicsNoConfigured: 'No hay gráficos configurados en este bundle.',
|
||||||
@@ -257,19 +306,31 @@ const messages: Record<Locale, Translations> = {
|
|||||||
graphicsScoreboard: 'Scoreboard',
|
graphicsScoreboard: 'Scoreboard',
|
||||||
graphicsCommentary: 'Comentario',
|
graphicsCommentary: 'Comentario',
|
||||||
graphicsSkinLabel: 'Skin',
|
graphicsSkinLabel: 'Skin',
|
||||||
|
graphicsCopied: 'URL copiada al portapapeles',
|
||||||
|
graphicsOpenBrowser: 'Abrir en el navegador',
|
||||||
|
|
||||||
|
// ── Commentary ───────────────────────────────────────────────────────────
|
||||||
commentaryTitle: 'Comentario',
|
commentaryTitle: 'Comentario',
|
||||||
commentaryCommentator1: 'Comentarista #1',
|
commentaryCommentator1: 'Comentarista #1',
|
||||||
commentaryCommentator2: 'Comentarista #2',
|
commentaryCommentator2: 'Comentarista #2',
|
||||||
commentaryTwitterText: '@Twitter / Texto',
|
commentaryTwitterText: '@Twitter / Texto',
|
||||||
|
commentaryTwitterMaxLength: 'Se excedió el límite de caracteres de Twitter',
|
||||||
|
commentaryTwitterInvalidChars: 'Caracteres inválidos en el texto de Twitter',
|
||||||
|
commentarySwap: 'Intercambiar comentaristas',
|
||||||
|
commentaryClear: 'Limpiar comentario',
|
||||||
|
|
||||||
|
// ── Bracket ──────────────────────────────────────────────────────────────
|
||||||
bracketTitle: 'Bracket',
|
bracketTitle: 'Bracket',
|
||||||
bracketStage: 'Etapa',
|
bracketStage: 'Etapa',
|
||||||
bracketSide: 'Lado del bracket',
|
bracketSide: 'Lado del bracket',
|
||||||
bracketCustomProgress: 'Progreso personalizado',
|
bracketCustomProgress: 'Progreso personalizado',
|
||||||
bracketPreview: 'Vista previa',
|
bracketPreview: 'Vista previa',
|
||||||
|
|
||||||
|
// ── Players ──────────────────────────────────────────────────────────────
|
||||||
playersLabelTeam: 'Equipo',
|
playersLabelTeam: 'Equipo',
|
||||||
playersLabelCountry: 'País',
|
playersLabelCountry: 'País',
|
||||||
playersLabelActions: 'Acciones',
|
playersLabelActions: 'Acciones',
|
||||||
playersStartggHelp: 'Conéctate por OAuth (recomendado) o pega tu token personal para cargar torneos que creaste o administras. Si ves "Client authentication failed", revisa que tu configuración use el Client ID/Secret de una app OAuth de start.gg.',
|
playersStartggHelp: 'Conéctate por OAuth (recomendado) o pega tu token personal para cargar torneos que creaste o administras.',
|
||||||
playersConnectStartgg: 'Conectar con start.gg',
|
playersConnectStartgg: 'Conectar con start.gg',
|
||||||
playersConnected: 'Conectado',
|
playersConnected: 'Conectado',
|
||||||
playersUsePersonalApi: 'Usar API personal',
|
playersUsePersonalApi: 'Usar API personal',
|
||||||
@@ -281,28 +342,15 @@ const messages: Record<Locale, Translations> = {
|
|||||||
playersSearchPlaceholder: 'Buscar...',
|
playersSearchPlaceholder: 'Buscar...',
|
||||||
playersImport: 'Importar',
|
playersImport: 'Importar',
|
||||||
playersExport: 'Exportar',
|
playersExport: 'Exportar',
|
||||||
commentaryTwitterMaxLength: 'Se excedió el límite de caracteres de Twitter',
|
playersConnectInSettings: 'Conecta tu cuenta en',
|
||||||
commentaryTwitterInvalidChars: 'Caracteres inválidos en el texto de Twitter',
|
playersConnectInSettingsSuffix: 'para importar jugadores desde torneos.',
|
||||||
commentarySwap: 'Intercambiar comentaristas',
|
|
||||||
commentaryClear: 'Limpiar comentario',
|
|
||||||
aboutChangelog: 'Changelog',
|
|
||||||
aboutTechStackTitle: 'Tech stack',
|
|
||||||
settingsShortcutConflictWarning: 'This shortcut is already assigned to',
|
|
||||||
settingsShortcutStartRecording: 'Start recording shortcut',
|
|
||||||
settingsShortcutStopRecording: 'Stop recording shortcut',
|
|
||||||
settingsShortcutResetSingle: 'Reset single player score shortcut',
|
|
||||||
graphicsCopied: 'URL copiada al portapapeles',
|
|
||||||
graphicsOpenBrowser: 'Abrir en el navegador',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeLocale = (value: unknown): Locale => (value === 'es' ? 'es' : 'en');
|
const normalizeLocale = (value: unknown): Locale => (value === 'es' ? 'es' : 'en');
|
||||||
|
|
||||||
const getStoredLocale = (): Locale => {
|
const getStoredLocale = (): Locale => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') return 'en';
|
||||||
return 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeLocale(localStorage.getItem(STORAGE_KEY));
|
return normalizeLocale(localStorage.getItem(STORAGE_KEY));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,7 +358,6 @@ export const locale = ref<Locale>(getStoredLocale());
|
|||||||
|
|
||||||
export const setLocale = (value: Locale) => {
|
export const setLocale = (value: Locale) => {
|
||||||
locale.value = normalizeLocale(value);
|
locale.value = normalizeLocale(value);
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.setItem(STORAGE_KEY, locale.value);
|
localStorage.setItem(STORAGE_KEY, locale.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const playersStore = usePlayersStore();
|
|||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const rows = computed<PlayerRow[]>(() => playersStore.rows);
|
const rows = computed<PlayerRow[]>(() => playersStore.rows);
|
||||||
|
|
||||||
// ─── Integraciones ─────────────────────────────────────────────────────────────
|
// ─── Integraciones (solo se usa para importar torneos) ─────────────────────────
|
||||||
|
|
||||||
const startgg = useIntegration({
|
const startgg = useIntegration({
|
||||||
messagePrefix: 'startgg',
|
messagePrefix: 'startgg',
|
||||||
@@ -57,7 +57,6 @@ const challonge = useIntegration({
|
|||||||
playersStore,
|
playersStore,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notifica errores de apertura del diálogo de importación (sustituye window.alert)
|
|
||||||
watch(() => startgg.importDialogError, (msg) => {
|
watch(() => startgg.importDialogError, (msg) => {
|
||||||
if (msg) $q.notify({ type: 'negative', message: msg });
|
if (msg) $q.notify({ type: 'negative', message: msg });
|
||||||
});
|
});
|
||||||
@@ -83,12 +82,6 @@ const formatExpiresAt = (ts: number): string =>
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Label de conexión de Challonge ───────────────────────────────────────────
|
|
||||||
|
|
||||||
const challongeConnectionLabel = computed(() =>
|
|
||||||
challonge.hasValidatedToken ? t('playersConnected') : 'Token set',
|
|
||||||
);
|
|
||||||
|
|
||||||
// ─── Tabla de jugadores ────────────────────────────────────────────────────────
|
// ─── Tabla de jugadores ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const filter = ref('');
|
const filter = ref('');
|
||||||
@@ -152,7 +145,6 @@ const openCreateDialog = () => {
|
|||||||
|
|
||||||
const openEditDialog = (row: PlayerRow) => {
|
const openEditDialog = (row: PlayerRow) => {
|
||||||
editingId.value = row.id;
|
editingId.value = row.id;
|
||||||
// CORRECCIÓN: evitar el patrón `void id` usando un alias _id
|
|
||||||
const { id: _id, ...playerData } = row;
|
const { id: _id, ...playerData } = row;
|
||||||
Object.assign(form, playerData);
|
Object.assign(form, playerData);
|
||||||
isDialogOpen.value = true;
|
isDialogOpen.value = true;
|
||||||
@@ -169,34 +161,6 @@ const deletePlayer = (row: PlayerRow) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Diálogos de token manual ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const isStartggManualDialogOpen = ref(false);
|
|
||||||
const startggManualDraft = ref('');
|
|
||||||
|
|
||||||
const openStartggManualDialog = () => {
|
|
||||||
startggManualDraft.value = startgg.token;
|
|
||||||
isStartggManualDialogOpen.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveStartggManualToken = () => {
|
|
||||||
startgg.token = startggManualDraft.value.trim();
|
|
||||||
isStartggManualDialogOpen.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isChallongeManualDialogOpen = ref(false);
|
|
||||||
const challongeManualDraft = ref('');
|
|
||||||
|
|
||||||
const openChallongeManualDialog = () => {
|
|
||||||
challongeManualDraft.value = challonge.token;
|
|
||||||
isChallongeManualDialogOpen.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveChallongeManualToken = () => {
|
|
||||||
challonge.token = challongeManualDraft.value.trim();
|
|
||||||
isChallongeManualDialogOpen.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Exportar / importar JSON ──────────────────────────────────────────────────
|
// ─── Exportar / importar JSON ──────────────────────────────────────────────────
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement | null>(null);
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
@@ -232,6 +196,8 @@ const handleImport = async (event: Event) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<QPage class="q-pa-lg players-page">
|
<QPage class="q-pa-lg players-page">
|
||||||
|
|
||||||
|
<!-- ── Cabecera ────────────────────────────────────────────────────────── -->
|
||||||
<div class="row items-center q-mb-md">
|
<div class="row items-center q-mb-md">
|
||||||
<div class="text-h5 text-weight-medium">
|
<div class="text-h5 text-weight-medium">
|
||||||
{{ t('menuPlayers') }}
|
{{ t('menuPlayers') }}
|
||||||
@@ -248,7 +214,9 @@ const handleImport = async (event: Event) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="players-content row q-col-gutter-md">
|
<div class="players-content row q-col-gutter-md">
|
||||||
<div class="col-12">
|
|
||||||
|
<!-- ── Columna principal: tabla ────────────────────────────────────── -->
|
||||||
|
<div class="col-12 col-lg-8 players-main-column">
|
||||||
<div class="row items-center q-gutter-sm q-mb-md">
|
<div class="row items-center q-gutter-sm q-mb-md">
|
||||||
<QInput
|
<QInput
|
||||||
v-model="filter"
|
v-model="filter"
|
||||||
@@ -262,19 +230,14 @@ const handleImport = async (event: Event) => {
|
|||||||
</template>
|
</template>
|
||||||
</QInput>
|
</QInput>
|
||||||
<span class="text-caption text-grey-6">{{ rows.length }} players</span>
|
<span class="text-caption text-grey-6">{{ rows.length }} players</span>
|
||||||
|
<QSpace />
|
||||||
<QBtn
|
<QBtn
|
||||||
color="secondary"
|
color="secondary" outline icon="file_upload" no-caps
|
||||||
outline
|
|
||||||
icon="file_upload"
|
|
||||||
no-caps
|
|
||||||
:label="t('playersImport')"
|
:label="t('playersImport')"
|
||||||
@click="triggerImport"
|
@click="triggerImport"
|
||||||
/>
|
/>
|
||||||
<QBtn
|
<QBtn
|
||||||
color="secondary"
|
color="secondary" outline icon="file_download" no-caps
|
||||||
outline
|
|
||||||
icon="file_download"
|
|
||||||
no-caps
|
|
||||||
:label="t('playersExport')"
|
:label="t('playersExport')"
|
||||||
@click="exportPlayers"
|
@click="exportPlayers"
|
||||||
/>
|
/>
|
||||||
@@ -286,9 +249,7 @@ const handleImport = async (event: Event) => {
|
|||||||
@change="handleImport"
|
@change="handleImport"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-lg-8 players-main-column">
|
|
||||||
<QTable
|
<QTable
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
@@ -328,110 +289,75 @@ const handleImport = async (event: Event) => {
|
|||||||
</QTooltip>
|
</QTooltip>
|
||||||
</QChip>
|
</QChip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="row.name" class="text-caption text-grey-6">
|
||||||
v-if="row.name"
|
|
||||||
class="text-caption text-grey-6"
|
|
||||||
>
|
|
||||||
{{ row.name }}
|
{{ row.name }}
|
||||||
</div>
|
</div>
|
||||||
</QTd>
|
</QTd>
|
||||||
</template>
|
</template>
|
||||||
<template #body-cell-actions="{ row }">
|
<template #body-cell-actions="{ row }">
|
||||||
<QTd align="right">
|
<QTd align="right">
|
||||||
<QBtn
|
<QBtn size="sm" flat icon="edit" @click="openEditDialog(row)" />
|
||||||
size="sm"
|
<QBtn size="sm" flat color="negative" icon="delete" @click="deletePlayer(row)" />
|
||||||
flat
|
|
||||||
icon="edit"
|
|
||||||
@click="openEditDialog(row)"
|
|
||||||
/>
|
|
||||||
<QBtn
|
|
||||||
size="sm"
|
|
||||||
flat
|
|
||||||
color="negative"
|
|
||||||
icon="delete"
|
|
||||||
@click="deletePlayer(row)"
|
|
||||||
/>
|
|
||||||
</QTd>
|
</QTd>
|
||||||
</template>
|
</template>
|
||||||
</QTable>
|
</QTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panel de integraciones -->
|
<!-- ── Columna lateral: importar desde torneo ───────────────────────── -->
|
||||||
<div class="col-12 col-lg-4 players-startgg-column">
|
<div class="col-12 col-lg-4 players-import-column">
|
||||||
<div class="players-integrations-stack">
|
<div class="players-integrations-stack">
|
||||||
|
|
||||||
<!-- ── start.gg ─────────────────────────────────────────────────── -->
|
<!-- start.gg -->
|
||||||
<QCard flat bordered class="q-pa-md">
|
<QCard flat bordered class="q-pa-md">
|
||||||
<div class="text-h6 q-mb-sm startgg-heading">
|
<div class="row items-center q-mb-xs q-gutter-x-sm">
|
||||||
<svg class="startgg-heading__icon" viewBox="0 0 40 40" fill="none" aria-hidden="true">
|
<svg style="width: 18px; height: 18px; flex-shrink: 0;" viewBox="0 0 40 40" fill="none" aria-hidden="true">
|
||||||
<path d="M1.25 20h7.5A1.25 1.25 0 0 0 10 18.75v-7.5A1.25 1.25 0 0 1 11.25 10h27.5A1.25 1.25 0 0 0 40 8.75V1.25A1.25 1.25 0 0 0 38.75 0H10A10 10 0 0 0 0 10v8.75A1.25 1.25 0 0 0 1.25 20Z" fill="#3f80ff" />
|
<path d="M1.25 20h7.5A1.25 1.25 0 0 0 10 18.75v-7.5A1.25 1.25 0 0 1 11.25 10h27.5A1.25 1.25 0 0 0 40 8.75V1.25A1.25 1.25 0 0 0 38.75 0H10A10 10 0 0 0 0 10v8.75A1.25 1.25 0 0 0 1.25 20Z" fill="#3f80ff" />
|
||||||
<path d="M38.75 20h-7.5A1.25 1.25 0 0 0 30 21.25v7.5A1.25 1.25 0 0 1 28.75 30H1.25A1.25 1.25 0 0 0 0 31.25v7.5A1.25 1.25 0 0 0 1.25 40H30A10 10 0 0 0 40 30V21.25A1.25 1.25 0 0 0 38.75 20Z" fill="#ff2768" />
|
<path d="M38.75 20h-7.5A1.25 1.25 0 0 0 30 21.25v7.5A1.25 1.25 0 0 1 28.75 30H1.25A1.25 1.25 0 0 0 0 31.25v7.5A1.25 1.25 0 0 0 1.25 40H30A10 10 0 0 0 40 30V21.25A1.25 1.25 0 0 0 38.75 20Z" fill="#ff2768" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>start.gg</span>
|
<span class="text-subtitle2">start.gg</span>
|
||||||
|
<QSpace />
|
||||||
|
<QChip
|
||||||
|
v-if="startgg.hasTokenConfigured"
|
||||||
|
dense size="sm"
|
||||||
|
:color="startgg.hasValidatedToken ? 'positive' : 'warning'"
|
||||||
|
text-color="white"
|
||||||
|
>
|
||||||
|
{{ t('playersConnected') }}
|
||||||
|
</QChip>
|
||||||
|
<QChip v-else dense size="sm" color="grey-7" text-color="white" icon="link_off">
|
||||||
|
{{ t('settingsNotConnected') || 'Not connected' }}
|
||||||
|
</QChip>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-grey-6 q-mb-md">
|
|
||||||
{{ t('playersStartggHelp') }}
|
<!-- Sin token: aviso con enlace a Settings -->
|
||||||
|
<div v-if="!startgg.hasTokenConfigured" class="text-caption text-grey-6 q-mt-sm">
|
||||||
|
{{ t('playersConnectInSettings') || 'Connect your start.gg account in' }}
|
||||||
|
<RouterLink to="/settings" class="text-primary">Settings</RouterLink>
|
||||||
|
{{ t('playersConnectInSettingsSuffix') || 'to import players from tournaments.' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-sm items-center">
|
|
||||||
<div class="col-auto">
|
<!-- Con token: selector de torneo -->
|
||||||
<QBtn
|
<template v-else>
|
||||||
v-if="!startgg.hasTokenConfigured"
|
<div v-if="startgg.tournamentsError" class="text-negative text-caption q-mt-xs">
|
||||||
color="primary"
|
|
||||||
icon="login"
|
|
||||||
no-caps
|
|
||||||
:label="t('playersConnectStartgg')"
|
|
||||||
:loading="startgg.oauthLoading"
|
|
||||||
@click="startgg.connectWithOAuth"
|
|
||||||
/>
|
|
||||||
<QBtn
|
|
||||||
v-else
|
|
||||||
outline
|
|
||||||
color="positive"
|
|
||||||
icon="check_circle"
|
|
||||||
no-caps
|
|
||||||
:label="t('playersConnected')"
|
|
||||||
class="startgg-connected-btn"
|
|
||||||
@click="openStartggManualDialog"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<QBtn
|
|
||||||
outline
|
|
||||||
color="white"
|
|
||||||
icon="vpn_key"
|
|
||||||
no-caps
|
|
||||||
:label="t('playersUsePersonalApi')"
|
|
||||||
@click="openStartggManualDialog"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="startgg.tournamentsError" class="text-negative q-mt-sm">
|
|
||||||
{{ startgg.tournamentsError }}
|
{{ startgg.tournamentsError }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row items-center q-mt-md startgg-tournament-row">
|
<div class="row items-center q-mt-sm players-tournament-row">
|
||||||
<QBtn
|
<QBtn
|
||||||
flat round dense
|
flat round dense text-color="white" icon="sync"
|
||||||
text-color="white"
|
class="players-refresh-btn"
|
||||||
icon="sync"
|
|
||||||
class="startgg-refresh-btn"
|
|
||||||
:loading="startgg.loadingTournaments"
|
:loading="startgg.loadingTournaments"
|
||||||
@click="startgg.loadRecentTournaments"
|
@click="startgg.loadRecentTournaments"
|
||||||
/>
|
>
|
||||||
|
<QTooltip>Refresh tournaments</QTooltip>
|
||||||
|
</QBtn>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<QSelect
|
<QSelect
|
||||||
v-model="startgg.selectedTournamentSlug"
|
v-model="startgg.selectedTournamentSlug"
|
||||||
v-model:input-value="startgg.tournamentInput"
|
v-model:input-value="startgg.tournamentInput"
|
||||||
:options="startgg.filteredTournamentOptions"
|
:options="startgg.filteredTournamentOptions"
|
||||||
option-value="value"
|
option-value="value" option-label="label"
|
||||||
option-label="label"
|
emit-value map-options use-input hide-selected fill-input
|
||||||
emit-value
|
input-debounce="0" clearable dense
|
||||||
map-options
|
|
||||||
use-input
|
|
||||||
hide-selected
|
|
||||||
fill-input
|
|
||||||
input-debounce="0"
|
|
||||||
clearable
|
|
||||||
dense
|
|
||||||
:label="t('playersTournament')"
|
:label="t('playersTournament')"
|
||||||
class="players-underlined-field"
|
class="players-underlined-field"
|
||||||
@filter="startgg.filterTournaments"
|
@filter="startgg.filterTournaments"
|
||||||
@@ -446,12 +372,9 @@ const handleImport = async (event: Event) => {
|
|||||||
</template>
|
</template>
|
||||||
</QSelect>
|
</QSelect>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="startgg.canImportSelectedTournament" class="col-auto">
|
<div v-if="startgg.canImportSelectedTournament" class="col-auto q-ml-xs">
|
||||||
<QBtn
|
<QBtn
|
||||||
color="primary"
|
color="primary" unelevated round icon="person_add"
|
||||||
unelevated
|
|
||||||
round
|
|
||||||
icon="person_add"
|
|
||||||
:aria-label="t('playersImportPlayers')"
|
:aria-label="t('playersImportPlayers')"
|
||||||
@click="startgg.openSelectedTournamentImportDialog"
|
@click="startgg.openSelectedTournamentImportDialog"
|
||||||
>
|
>
|
||||||
@@ -459,80 +382,57 @@ const handleImport = async (event: Event) => {
|
|||||||
</QBtn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</QCard>
|
</QCard>
|
||||||
|
|
||||||
<!-- ── Challonge ──────────────────────────────────────────────────── -->
|
<!-- Challonge -->
|
||||||
<QCard flat bordered class="q-pa-md">
|
<QCard flat bordered class="q-pa-md">
|
||||||
<div class="text-h6 q-mb-sm startgg-heading">
|
<div class="row items-center q-mb-xs q-gutter-x-sm">
|
||||||
<img
|
<img src="https://challonge.com/favicon.ico" alt="Challonge" style="width: 18px; height: 18px; border-radius: 4px; flex-shrink: 0;">
|
||||||
class="challonge-heading__icon"
|
<span class="text-subtitle2">Challonge</span>
|
||||||
src="https://challonge.com/favicon.ico"
|
<QSpace />
|
||||||
alt="Challonge"
|
<QChip
|
||||||
>
|
v-if="challonge.hasTokenConfigured"
|
||||||
<span>Challonge</span>
|
dense size="sm"
|
||||||
</div>
|
|
||||||
<div class="text-caption text-grey-6 q-mb-md">
|
|
||||||
{{ t('playersChallongeHelp') }}
|
|
||||||
</div>
|
|
||||||
<div class="row q-col-gutter-sm items-center">
|
|
||||||
<div class="col-auto">
|
|
||||||
<QBtn
|
|
||||||
v-if="!challonge.hasTokenConfigured"
|
|
||||||
color="primary"
|
|
||||||
icon="login"
|
|
||||||
no-caps
|
|
||||||
:label="t('playersConnectChallonge')"
|
|
||||||
:loading="challonge.oauthLoading"
|
|
||||||
@click="challonge.connectWithOAuth"
|
|
||||||
/>
|
|
||||||
<QBtn
|
|
||||||
v-else
|
|
||||||
outline
|
|
||||||
:color="challonge.hasValidatedToken ? 'positive' : 'warning'"
|
:color="challonge.hasValidatedToken ? 'positive' : 'warning'"
|
||||||
icon="check_circle"
|
text-color="white"
|
||||||
no-caps
|
>
|
||||||
:label="challongeConnectionLabel"
|
{{ challonge.hasValidatedToken ? t('playersConnected') : 'Token set' }}
|
||||||
@click="openChallongeManualDialog"
|
</QChip>
|
||||||
/>
|
<QChip v-else dense size="sm" color="grey-7" text-color="white" icon="link_off">
|
||||||
|
{{ t('settingsNotConnected') || 'Not connected' }}
|
||||||
|
</QChip>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
|
||||||
<QBtn
|
<!-- Sin token: aviso -->
|
||||||
outline
|
<div v-if="!challonge.hasTokenConfigured" class="text-caption text-grey-6 q-mt-sm">
|
||||||
color="white"
|
{{ t('playersConnectInSettings') || 'Connect your Challonge account in' }}
|
||||||
icon="vpn_key"
|
<RouterLink to="/settings" class="text-primary">Settings</RouterLink>
|
||||||
no-caps
|
{{ t('playersConnectInSettingsSuffix') || 'to import players from tournaments.' }}
|
||||||
:label="t('playersUsePersonalApi')"
|
|
||||||
@click="openChallongeManualDialog"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div v-if="challonge.tournamentsError" class="text-negative q-mt-sm">
|
<!-- Con token: selector de torneo -->
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="challonge.tournamentsError" class="text-negative text-caption q-mt-xs">
|
||||||
{{ challonge.tournamentsError }}
|
{{ challonge.tournamentsError }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row items-center q-mt-md startgg-tournament-row">
|
<div class="row items-center q-mt-sm players-tournament-row">
|
||||||
<QBtn
|
<QBtn
|
||||||
flat round dense
|
flat round dense text-color="white" icon="sync"
|
||||||
text-color="white"
|
class="players-refresh-btn"
|
||||||
icon="sync"
|
|
||||||
class="startgg-refresh-btn"
|
|
||||||
:loading="challonge.loadingTournaments"
|
:loading="challonge.loadingTournaments"
|
||||||
@click="challonge.loadRecentTournaments"
|
@click="challonge.loadRecentTournaments"
|
||||||
/>
|
>
|
||||||
|
<QTooltip>Refresh tournaments</QTooltip>
|
||||||
|
</QBtn>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<QSelect
|
<QSelect
|
||||||
v-model="challonge.selectedTournamentSlug"
|
v-model="challonge.selectedTournamentSlug"
|
||||||
v-model:input-value="challonge.tournamentInput"
|
v-model:input-value="challonge.tournamentInput"
|
||||||
:options="challonge.filteredTournamentOptions"
|
:options="challonge.filteredTournamentOptions"
|
||||||
option-value="value"
|
option-value="value" option-label="label"
|
||||||
option-label="label"
|
emit-value map-options use-input hide-selected fill-input
|
||||||
emit-value
|
input-debounce="0" clearable dense
|
||||||
map-options
|
|
||||||
use-input
|
|
||||||
hide-selected
|
|
||||||
fill-input
|
|
||||||
input-debounce="0"
|
|
||||||
clearable
|
|
||||||
dense
|
|
||||||
:label="t('playersTournament')"
|
:label="t('playersTournament')"
|
||||||
class="players-underlined-field"
|
class="players-underlined-field"
|
||||||
@filter="challonge.filterTournaments"
|
@filter="challonge.filterTournaments"
|
||||||
@@ -547,12 +447,9 @@ const handleImport = async (event: Event) => {
|
|||||||
</template>
|
</template>
|
||||||
</QSelect>
|
</QSelect>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="challonge.canImportSelectedTournament" class="col-auto">
|
<div v-if="challonge.canImportSelectedTournament" class="col-auto q-ml-xs">
|
||||||
<QBtn
|
<QBtn
|
||||||
color="primary"
|
color="primary" unelevated round icon="person_add"
|
||||||
unelevated
|
|
||||||
round
|
|
||||||
icon="person_add"
|
|
||||||
:aria-label="t('playersImportPlayers')"
|
:aria-label="t('playersImportPlayers')"
|
||||||
@click="challonge.openSelectedTournamentImportDialog"
|
@click="challonge.openSelectedTournamentImportDialog"
|
||||||
>
|
>
|
||||||
@@ -560,6 +457,7 @@ const handleImport = async (event: Event) => {
|
|||||||
</QBtn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</QCard>
|
</QCard>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -570,9 +468,7 @@ const handleImport = async (event: Event) => {
|
|||||||
<QDialog v-model="startgg.importDialogOpen">
|
<QDialog v-model="startgg.importDialogOpen">
|
||||||
<QCard class="players-dialog">
|
<QCard class="players-dialog">
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-h6">
|
<div class="text-h6">Import from {{ startgg.importingTournament?.name || 'start.gg' }}</div>
|
||||||
Import from {{ startgg.importingTournament?.name || 'start.gg' }}
|
|
||||||
</div>
|
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
<QSeparator />
|
<QSeparator />
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
@@ -617,9 +513,7 @@ const handleImport = async (event: Event) => {
|
|||||||
<QDialog v-model="challonge.importDialogOpen">
|
<QDialog v-model="challonge.importDialogOpen">
|
||||||
<QCard class="players-dialog">
|
<QCard class="players-dialog">
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-h6">
|
<div class="text-h6">Import from {{ challonge.importingTournament?.name || 'Challonge' }}</div>
|
||||||
Import from {{ challonge.importingTournament?.name || 'Challonge' }}
|
|
||||||
</div>
|
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
<QSeparator />
|
<QSeparator />
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
@@ -660,65 +554,6 @@ const handleImport = async (event: Event) => {
|
|||||||
</QCard>
|
</QCard>
|
||||||
</QDialog>
|
</QDialog>
|
||||||
|
|
||||||
<!-- ── Diálogo token personal start.gg ────────────────────────────────── -->
|
|
||||||
<QDialog v-model="isStartggManualDialogOpen">
|
|
||||||
<QCard class="players-dialog">
|
|
||||||
<QCardSection>
|
|
||||||
<div class="text-h6">Personal start.gg API</div>
|
|
||||||
</QCardSection>
|
|
||||||
<QSeparator />
|
|
||||||
<QCardSection>
|
|
||||||
<div class="text-body2 q-mb-sm">
|
|
||||||
If OAuth fails, you can create your personal token manually with these steps:
|
|
||||||
</div>
|
|
||||||
<ol class="q-pl-md q-mb-md manual-token-steps">
|
|
||||||
<li>Go to https://start.gg/admin/profile/developer</li>
|
|
||||||
<li>Sign in with your account</li>
|
|
||||||
<li>From the 3 access tokens, click <strong>Third Party</strong></li>
|
|
||||||
<li>Create a new one and fill the description with any name you want</li>
|
|
||||||
<li>Copy the generated token and paste it into Scoreko</li>
|
|
||||||
</ol>
|
|
||||||
<QInput
|
|
||||||
v-model="startggManualDraft"
|
|
||||||
label="Paste your personal token"
|
|
||||||
dense outlined type="password"
|
|
||||||
/>
|
|
||||||
</QCardSection>
|
|
||||||
<QSeparator />
|
|
||||||
<QCardActions align="right">
|
|
||||||
<QBtn flat no-caps label="Cancel" color="secondary" @click="isStartggManualDialogOpen = false" />
|
|
||||||
<QBtn flat no-caps color="negative" label="Delete token" @click="startggManualDraft = ''; saveStartggManualToken()" />
|
|
||||||
<QBtn no-caps color="primary" label="Save token" @click="saveStartggManualToken" />
|
|
||||||
</QCardActions>
|
|
||||||
</QCard>
|
|
||||||
</QDialog>
|
|
||||||
|
|
||||||
<!-- ── Diálogo token personal Challonge ───────────────────────────────── -->
|
|
||||||
<QDialog v-model="isChallongeManualDialogOpen">
|
|
||||||
<QCard class="players-dialog">
|
|
||||||
<QCardSection>
|
|
||||||
<div class="text-h6">Personal Challonge API</div>
|
|
||||||
</QCardSection>
|
|
||||||
<QSeparator />
|
|
||||||
<QCardSection>
|
|
||||||
<div class="text-body2 q-mb-sm">
|
|
||||||
If OAuth fails, paste a personal Challonge API token.
|
|
||||||
</div>
|
|
||||||
<QInput
|
|
||||||
v-model="challongeManualDraft"
|
|
||||||
label="Paste your personal Challonge token"
|
|
||||||
dense outlined type="password"
|
|
||||||
/>
|
|
||||||
</QCardSection>
|
|
||||||
<QSeparator />
|
|
||||||
<QCardActions align="right">
|
|
||||||
<QBtn flat no-caps label="Cancel" color="secondary" @click="isChallongeManualDialogOpen = false" />
|
|
||||||
<QBtn flat no-caps color="negative" label="Delete token" @click="challongeManualDraft = ''; saveChallongeManualToken()" />
|
|
||||||
<QBtn no-caps color="primary" label="Save token" @click="saveChallongeManualToken" />
|
|
||||||
</QCardActions>
|
|
||||||
</QCard>
|
|
||||||
</QDialog>
|
|
||||||
|
|
||||||
<!-- ── Diálogo crear / editar jugador ─────────────────────────────────── -->
|
<!-- ── Diálogo crear / editar jugador ─────────────────────────────────── -->
|
||||||
<QDialog v-model="isDialogOpen">
|
<QDialog v-model="isDialogOpen">
|
||||||
<QCard class="players-dialog">
|
<QCard class="players-dialog">
|
||||||
@@ -750,18 +585,11 @@ const handleImport = async (event: Event) => {
|
|||||||
v-model="form.country"
|
v-model="form.country"
|
||||||
v-model:input-value="countryInput"
|
v-model:input-value="countryInput"
|
||||||
:options="filteredCountryOptions"
|
:options="filteredCountryOptions"
|
||||||
option-value="value"
|
option-value="value" option-label="label"
|
||||||
option-label="label"
|
emit-value map-options use-input input-debounce="0"
|
||||||
emit-value
|
hide-selected fill-input clearable
|
||||||
map-options
|
|
||||||
use-input
|
|
||||||
input-debounce="0"
|
|
||||||
hide-selected
|
|
||||||
fill-input
|
|
||||||
clearable
|
|
||||||
:label="t('playersLabelCountry')"
|
:label="t('playersLabelCountry')"
|
||||||
dense
|
dense class="players-underlined-field"
|
||||||
class="players-underlined-field"
|
|
||||||
@filter="filterCountries"
|
@filter="filterCountries"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -781,6 +609,7 @@ const handleImport = async (event: Event) => {
|
|||||||
</QCardActions>
|
</QCardActions>
|
||||||
</QCard>
|
</QCard>
|
||||||
</QDialog>
|
</QDialog>
|
||||||
|
|
||||||
</QPage>
|
</QPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -804,8 +633,8 @@ const handleImport = async (event: Event) => {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.players-startgg-column {
|
.players-import-column {
|
||||||
min-width: 320px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.players-integrations-stack {
|
.players-integrations-stack {
|
||||||
@@ -819,35 +648,14 @@ const handleImport = async (event: Event) => {
|
|||||||
width: min(720px, 90vw);
|
width: min(720px, 90vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
.startgg-heading {
|
.players-tournament-row {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.startgg-heading__icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.challonge-heading__icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.startgg-tournament-row {
|
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.startgg-refresh-btn:hover {
|
.players-refresh-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.startgg-connected-btn {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.players-underlined-field :deep(.q-field__control) {
|
.players-underlined-field :deep(.q-field__control) {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -861,10 +669,6 @@ const handleImport = async (event: Event) => {
|
|||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.34);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
.manual-token-steps {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useHead } from '@unhead/vue';
|
import { useHead } from '@unhead/vue';
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
import { useQuasar } from 'quasar';
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { useIntegration } from '../composables/useIntegration';
|
||||||
import type { Locale } from '../i18n';
|
import type { Locale } from '../i18n';
|
||||||
import { locale, setLocale, t } from '../i18n';
|
import { locale, setLocale, t } from '../i18n';
|
||||||
|
import { usePlayersStore } from '../stores/players';
|
||||||
import {
|
import {
|
||||||
eventToShortcut,
|
eventToShortcut,
|
||||||
type ShortcutAction,
|
type ShortcutAction,
|
||||||
@@ -13,6 +16,8 @@ defineOptions({ name: 'SettingsView' });
|
|||||||
|
|
||||||
useHead(() => ({ title: t('settingsTitle') }));
|
useHead(() => ({ title: t('settingsTitle') }));
|
||||||
|
|
||||||
|
// ─── Idioma ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const languageOptions = computed(() => [
|
const languageOptions = computed(() => [
|
||||||
{ label: t('languageSpanish'), value: 'es' as const },
|
{ label: t('languageSpanish'), value: 'es' as const },
|
||||||
{ label: t('languageEnglish'), value: 'en' as const },
|
{ label: t('languageEnglish'), value: 'en' as const },
|
||||||
@@ -20,15 +25,13 @@ const languageOptions = computed(() => [
|
|||||||
|
|
||||||
const selectedLanguage = computed<Locale>({
|
const selectedLanguage = computed<Locale>({
|
||||||
get: () => locale.value,
|
get: () => locale.value,
|
||||||
set: (value) => {
|
set: (value) => { setLocale(value); },
|
||||||
setLocale(value);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Atajos de teclado ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const shortcutSettingsStore = useShortcutSettingsStore();
|
const shortcutSettingsStore = useShortcutSettingsStore();
|
||||||
const recordingAction = ref<ShortcutAction | null>(null);
|
const recordingAction = ref<ShortcutAction | null>(null);
|
||||||
|
|
||||||
// Ref para detectar clicks fuera del contenedor de atajos
|
|
||||||
const shortcutsContainerRef = ref<HTMLElement | null>(null);
|
const shortcutsContainerRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const shortcutFields = computed<{ action: ShortcutAction; label: string; hint: string }[]>(() => [
|
const shortcutFields = computed<{ action: ShortcutAction; label: string; hint: string }[]>(() => [
|
||||||
@@ -38,7 +41,6 @@ const shortcutFields = computed<{ action: ShortcutAction; label: string; hint: s
|
|||||||
{ action: 'rightDecrement', label: t('settingsShortcutRightDecrementLabel'), hint: t('settingsShortcutRightDecrementHint') },
|
{ action: 'rightDecrement', label: t('settingsShortcutRightDecrementLabel'), hint: t('settingsShortcutRightDecrementHint') },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Detecta atajos duplicados entre acciones
|
|
||||||
const conflictingActions = computed(() => {
|
const conflictingActions = computed(() => {
|
||||||
const seen = new Map<string, ShortcutAction>();
|
const seen = new Map<string, ShortcutAction>();
|
||||||
const conflicts = new Set<ShortcutAction>();
|
const conflicts = new Set<ShortcutAction>();
|
||||||
@@ -62,43 +64,24 @@ const stopRecording = () => {
|
|||||||
|
|
||||||
const onRecordKeydown = (event: KeyboardEvent) => {
|
const onRecordKeydown = (event: KeyboardEvent) => {
|
||||||
if (!recordingAction.value) return;
|
if (!recordingAction.value) return;
|
||||||
|
if (event.key === 'Escape') { event.preventDefault(); stopRecording(); return; }
|
||||||
// Escape cancela la grabación sin asignar ningún atajo
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
stopRecording();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcut = eventToShortcut(event);
|
const shortcut = eventToShortcut(event);
|
||||||
if (!shortcut) return;
|
if (!shortcut) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shortcutSettingsStore.setShortcut(recordingAction.value, shortcut);
|
shortcutSettingsStore.setShortcut(recordingAction.value, shortcut);
|
||||||
stopRecording();
|
stopRecording();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Click fuera del área de atajos también cancela la grabación
|
|
||||||
const onDocumentMousedown = (event: MouseEvent) => {
|
const onDocumentMousedown = (event: MouseEvent) => {
|
||||||
if (
|
if (recordingAction.value && shortcutsContainerRef.value && !shortcutsContainerRef.value.contains(event.target as Node)) {
|
||||||
recordingAction.value &&
|
|
||||||
shortcutsContainerRef.value &&
|
|
||||||
!shortcutsContainerRef.value.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
stopRecording();
|
stopRecording();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startRecording = (action: ShortcutAction) => {
|
const startRecording = (action: ShortcutAction) => {
|
||||||
if (recordingAction.value === action) {
|
if (recordingAction.value === action) { stopRecording(); return; }
|
||||||
stopRecording();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordingAction.value = action;
|
recordingAction.value = action;
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') document.body.dataset.shortcutRecording = 'true';
|
||||||
document.body.dataset.shortcutRecording = 'true';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -113,6 +96,79 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
stopRecording();
|
stopRecording();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Integraciones ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STARTGG_TOKEN_STORAGE_KEY = 'scoreko-dev.startgg-token';
|
||||||
|
const CHALLONGE_TOKEN_STORAGE_KEY = 'scoreko-dev.challonge-token';
|
||||||
|
const STARTGG_TEMP_PLAYERS_STORAGE_KEY = 'scoreko-dev.startgg-temp-players';
|
||||||
|
const CHALLONGE_TEMP_PLAYERS_STORAGE_KEY = 'scoreko-dev.challonge-temp-players';
|
||||||
|
const TEMP_FALLBACK_DURATION_SECONDS = 12 * 60 * 60;
|
||||||
|
|
||||||
|
const playersStore = usePlayersStore();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const startgg = useIntegration({
|
||||||
|
messagePrefix: 'startgg',
|
||||||
|
providerLabel: 'start.gg',
|
||||||
|
tokenStorageKey: STARTGG_TOKEN_STORAGE_KEY,
|
||||||
|
tempPlayersStorageKey: STARTGG_TEMP_PLAYERS_STORAGE_KEY,
|
||||||
|
tempFallbackDurationSeconds: TEMP_FALLBACK_DURATION_SECONDS,
|
||||||
|
playersStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
const challonge = useIntegration({
|
||||||
|
messagePrefix: 'challonge',
|
||||||
|
providerLabel: 'Challonge',
|
||||||
|
tokenStorageKey: CHALLONGE_TOKEN_STORAGE_KEY,
|
||||||
|
tempPlayersStorageKey: CHALLONGE_TEMP_PLAYERS_STORAGE_KEY,
|
||||||
|
tempFallbackDurationSeconds: TEMP_FALLBACK_DURATION_SECONDS,
|
||||||
|
on401Message:
|
||||||
|
'Challonge rejected the token (401 Unauthorized). Re-connect OAuth so it grants scopes (me, tournaments:read, participants:read) or paste a valid personal API token.',
|
||||||
|
playersStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Diálogos de token manual ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const isStartggManualDialogOpen = ref(false);
|
||||||
|
const startggManualDraft = ref('');
|
||||||
|
|
||||||
|
const openStartggManualDialog = () => {
|
||||||
|
startggManualDraft.value = startgg.token;
|
||||||
|
isStartggManualDialogOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveStartggManualToken = () => {
|
||||||
|
startgg.token = startggManualDraft.value.trim();
|
||||||
|
isStartggManualDialogOpen.value = false;
|
||||||
|
$q.notify({ type: 'positive', message: startgg.token ? 'start.gg token saved.' : 'start.gg token removed.' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChallongeManualDialogOpen = ref(false);
|
||||||
|
const challongeManualDraft = ref('');
|
||||||
|
|
||||||
|
const openChallongeManualDialog = () => {
|
||||||
|
challongeManualDraft.value = challonge.token;
|
||||||
|
isChallongeManualDialogOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveChallongeManualToken = () => {
|
||||||
|
challonge.token = challongeManualDraft.value.trim();
|
||||||
|
isChallongeManualDialogOpen.value = false;
|
||||||
|
$q.notify({ type: 'positive', message: challonge.token ? 'Challonge token saved.' : 'Challonge token removed.' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Label de estado de Challonge
|
||||||
|
const challongeConnectionLabel = computed(() =>
|
||||||
|
challonge.hasValidatedToken ? t('playersConnected') : 'Token set',
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(() => startgg.importDialogError, (msg) => {
|
||||||
|
if (msg) $q.notify({ type: 'negative', message: msg });
|
||||||
|
});
|
||||||
|
watch(() => challonge.importDialogError, (msg) => {
|
||||||
|
if (msg) $q.notify({ type: 'negative', message: msg });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -126,17 +182,12 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<QCard
|
<div class="column q-gutter-lg settings-layout">
|
||||||
flat
|
|
||||||
bordered
|
<!-- ── Idioma ──────────────────────────────────────────────────────────── -->
|
||||||
class="settings-card"
|
<QCard flat bordered class="settings-card">
|
||||||
>
|
|
||||||
<!-- Language -->
|
|
||||||
<QCardSection class="q-pa-lg">
|
<QCardSection class="q-pa-lg">
|
||||||
<!--
|
<div class="text-overline text-grey-6 q-mb-md">{{ t('settingsLanguageLabel') }}</div>
|
||||||
Label movido al propio QSelect (más idiomático en Quasar con outlined).
|
|
||||||
Se elimina el text-overline redundante de encima.
|
|
||||||
-->
|
|
||||||
<QSelect
|
<QSelect
|
||||||
v-model="selectedLanguage"
|
v-model="selectedLanguage"
|
||||||
emit-value
|
emit-value
|
||||||
@@ -145,27 +196,148 @@ onBeforeUnmount(() => {
|
|||||||
:label="t('settingsLanguageLabel')"
|
:label="t('settingsLanguageLabel')"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
|
style="max-width: 280px"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="text-caption text-grey-6 q-mt-sm">
|
<div class="text-caption text-grey-6 q-mt-sm">
|
||||||
{{ t('settingsLanguageHint') }}
|
{{ t('settingsLanguageHint') }}
|
||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
|
</QCard>
|
||||||
|
|
||||||
|
<!-- ── Integraciones ───────────────────────────────────────────────────── -->
|
||||||
|
<QCard flat bordered class="settings-card">
|
||||||
|
<QCardSection class="q-pa-lg">
|
||||||
|
<div class="text-overline text-grey-6 q-mb-xs">{{ t('settingsIntegrationsTitle') || 'Integrations' }}</div>
|
||||||
|
<div class="text-caption text-grey-6 q-mb-lg">
|
||||||
|
{{ t('settingsIntegrationsDescription') || 'Connect your tournament platform accounts to import players directly from brackets.' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column q-gutter-md">
|
||||||
|
|
||||||
|
<!-- start.gg -->
|
||||||
|
<div class="integration-row">
|
||||||
|
<div class="integration-row__logo">
|
||||||
|
<svg style="width: 28px; height: 28px;" viewBox="0 0 40 40" fill="none" aria-hidden="true">
|
||||||
|
<path d="M1.25 20h7.5A1.25 1.25 0 0 0 10 18.75v-7.5A1.25 1.25 0 0 1 11.25 10h27.5A1.25 1.25 0 0 0 40 8.75V1.25A1.25 1.25 0 0 0 38.75 0H10A10 10 0 0 0 0 10v8.75A1.25 1.25 0 0 0 1.25 20Z" fill="#3f80ff" />
|
||||||
|
<path d="M38.75 20h-7.5A1.25 1.25 0 0 0 30 21.25v7.5A1.25 1.25 0 0 1 28.75 30H1.25A1.25 1.25 0 0 0 0 31.25v7.5A1.25 1.25 0 0 0 1.25 40H30A10 10 0 0 0 40 30V21.25A1.25 1.25 0 0 0 38.75 20Z" fill="#ff2768" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="integration-row__info">
|
||||||
|
<div class="text-body2 text-weight-medium">start.gg</div>
|
||||||
|
<div class="text-caption text-grey-6">{{ t('playersStartggHelp') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="integration-row__actions row q-gutter-sm items-center">
|
||||||
|
<QChip
|
||||||
|
v-if="startgg.hasTokenConfigured"
|
||||||
|
dense
|
||||||
|
:color="startgg.hasValidatedToken ? 'positive' : 'warning'"
|
||||||
|
text-color="white"
|
||||||
|
icon="check_circle"
|
||||||
|
>
|
||||||
|
{{ t('playersConnected') }}
|
||||||
|
</QChip>
|
||||||
|
<QBtn
|
||||||
|
v-if="!startgg.hasTokenConfigured"
|
||||||
|
color="primary"
|
||||||
|
icon="login"
|
||||||
|
no-caps
|
||||||
|
unelevated
|
||||||
|
:label="t('playersConnectStartgg')"
|
||||||
|
:loading="startgg.oauthLoading"
|
||||||
|
@click="startgg.connectWithOAuth"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
v-else
|
||||||
|
flat
|
||||||
|
color="negative"
|
||||||
|
icon="link_off"
|
||||||
|
no-caps
|
||||||
|
size="sm"
|
||||||
|
:label="t('settingsDisconnect') || 'Disconnect'"
|
||||||
|
@click="startggManualDraft = ''; startgg.token = ''; $q.notify({ type: 'info', message: 'start.gg disconnected.' })"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
outline
|
||||||
|
:color="startgg.hasTokenConfigured ? 'grey-5' : 'white'"
|
||||||
|
icon="vpn_key"
|
||||||
|
no-caps
|
||||||
|
size="sm"
|
||||||
|
:label="t('playersUsePersonalApi')"
|
||||||
|
@click="openStartggManualDialog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<QSeparator />
|
<QSeparator />
|
||||||
|
|
||||||
<!-- Shortcuts -->
|
<!-- Challonge -->
|
||||||
|
<div class="integration-row">
|
||||||
|
<div class="integration-row__logo">
|
||||||
|
<img
|
||||||
|
src="https://challonge.com/favicon.ico"
|
||||||
|
alt="Challonge"
|
||||||
|
style="width: 28px; height: 28px; border-radius: 6px;"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="integration-row__info">
|
||||||
|
<div class="text-body2 text-weight-medium">Challonge</div>
|
||||||
|
<div class="text-caption text-grey-6">{{ t('playersChallongeHelp') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="integration-row__actions row q-gutter-sm items-center">
|
||||||
|
<QChip
|
||||||
|
v-if="challonge.hasTokenConfigured"
|
||||||
|
dense
|
||||||
|
:color="challonge.hasValidatedToken ? 'positive' : 'warning'"
|
||||||
|
text-color="white"
|
||||||
|
icon="check_circle"
|
||||||
|
>
|
||||||
|
{{ challongeConnectionLabel }}
|
||||||
|
</QChip>
|
||||||
|
<QBtn
|
||||||
|
v-if="!challonge.hasTokenConfigured"
|
||||||
|
color="primary"
|
||||||
|
icon="login"
|
||||||
|
no-caps
|
||||||
|
unelevated
|
||||||
|
:label="t('playersConnectChallonge')"
|
||||||
|
:loading="challonge.oauthLoading"
|
||||||
|
@click="challonge.connectWithOAuth"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
v-else
|
||||||
|
flat
|
||||||
|
color="negative"
|
||||||
|
icon="link_off"
|
||||||
|
no-caps
|
||||||
|
size="sm"
|
||||||
|
:label="t('settingsDisconnect') || 'Disconnect'"
|
||||||
|
@click="challongeManualDraft = ''; challonge.token = ''; $q.notify({ type: 'info', message: 'Challonge disconnected.' })"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
outline
|
||||||
|
:color="challonge.hasTokenConfigured ? 'grey-5' : 'white'"
|
||||||
|
icon="vpn_key"
|
||||||
|
no-caps
|
||||||
|
size="sm"
|
||||||
|
:label="t('playersUsePersonalApi')"
|
||||||
|
@click="openChallongeManualDialog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
</QCard>
|
||||||
|
|
||||||
|
<!-- ── Atajos de teclado ───────────────────────────────────────────────── -->
|
||||||
|
<QCard flat bordered class="settings-card">
|
||||||
<QCardSection class="q-pa-lg">
|
<QCardSection class="q-pa-lg">
|
||||||
<div class="row items-center justify-between q-mb-xs">
|
<div class="row items-center justify-between q-mb-xs">
|
||||||
<div class="text-overline text-grey-6">
|
<div class="text-overline text-grey-6">
|
||||||
{{ t('settingsShortcutTitle') }}
|
{{ t('settingsShortcutTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<QBtn
|
<QBtn
|
||||||
round
|
round dense flat color="primary" icon="restart_alt"
|
||||||
dense
|
|
||||||
flat
|
|
||||||
color="primary"
|
|
||||||
icon="restart_alt"
|
|
||||||
:aria-label="t('settingsShortcutReset')"
|
:aria-label="t('settingsShortcutReset')"
|
||||||
@click="shortcutSettingsStore.resetShortcuts"
|
@click="shortcutSettingsStore.resetShortcuts"
|
||||||
>
|
>
|
||||||
@@ -177,12 +349,10 @@ onBeforeUnmount(() => {
|
|||||||
{{ t('settingsShortcutDescription') }}
|
{{ t('settingsShortcutDescription') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aviso de conflicto: se muestra si dos acciones comparten el mismo atajo -->
|
|
||||||
<QBanner
|
<QBanner
|
||||||
v-if="conflictingActions.size > 0"
|
v-if="conflictingActions.size > 0"
|
||||||
class="bg-warning text-white q-mb-md"
|
class="bg-warning text-white q-mb-md"
|
||||||
rounded
|
rounded dense
|
||||||
dense
|
|
||||||
>
|
>
|
||||||
<template #avatar>
|
<template #avatar>
|
||||||
<QIcon name="warning" color="white" />
|
<QIcon name="warning" color="white" />
|
||||||
@@ -190,14 +360,7 @@ onBeforeUnmount(() => {
|
|||||||
{{ t('settingsShortcutConflictWarning') }}
|
{{ t('settingsShortcutConflictWarning') }}
|
||||||
</QBanner>
|
</QBanner>
|
||||||
|
|
||||||
<!--
|
<div ref="shortcutsContainerRef" class="column q-gutter-md">
|
||||||
ref="shortcutsContainerRef" permite detectar clicks fuera
|
|
||||||
de esta área para cancelar la grabación automáticamente.
|
|
||||||
-->
|
|
||||||
<div
|
|
||||||
ref="shortcutsContainerRef"
|
|
||||||
class="column q-gutter-md"
|
|
||||||
>
|
|
||||||
<QInput
|
<QInput
|
||||||
v-for="field in shortcutFields"
|
v-for="field in shortcutFields"
|
||||||
:key="field.action"
|
:key="field.action"
|
||||||
@@ -210,35 +373,19 @@ onBeforeUnmount(() => {
|
|||||||
? 'warning'
|
? 'warning'
|
||||||
: 'primary'
|
: 'primary'
|
||||||
"
|
"
|
||||||
readonly
|
readonly outlined dense bottom-slots
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
bottom-slots
|
|
||||||
:label="field.label"
|
:label="field.label"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<!-- Botón grabar / detener -->
|
|
||||||
<QBtn
|
<QBtn
|
||||||
flat
|
flat round dense
|
||||||
round
|
|
||||||
dense
|
|
||||||
:icon="recordingAction === field.action ? 'stop_circle' : 'keyboard'"
|
:icon="recordingAction === field.action ? 'stop_circle' : 'keyboard'"
|
||||||
:color="recordingAction === field.action ? 'negative' : 'primary'"
|
:color="recordingAction === field.action ? 'negative' : 'primary'"
|
||||||
:aria-label="
|
:aria-label="recordingAction === field.action ? t('settingsShortcutStopRecording') : t('settingsShortcutStartRecording')"
|
||||||
recordingAction === field.action
|
|
||||||
? t('settingsShortcutStopRecording')
|
|
||||||
: t('settingsShortcutStartRecording')
|
|
||||||
"
|
|
||||||
@click="startRecording(field.action)"
|
@click="startRecording(field.action)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Botón reset individual por atajo -->
|
|
||||||
<QBtn
|
<QBtn
|
||||||
flat
|
flat round dense icon="restart_alt" color="grey-5"
|
||||||
round
|
|
||||||
dense
|
|
||||||
icon="restart_alt"
|
|
||||||
color="grey-5"
|
|
||||||
:aria-label="t('settingsShortcutResetSingle')"
|
:aria-label="t('settingsShortcutResetSingle')"
|
||||||
@click="shortcutSettingsStore.resetShortcut(field.action)"
|
@click="shortcutSettingsStore.resetShortcut(field.action)"
|
||||||
>
|
>
|
||||||
@@ -249,11 +396,110 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
</QCard>
|
</QCard>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Diálogo token personal start.gg ──────────────────────────────────── -->
|
||||||
|
<QDialog v-model="isStartggManualDialogOpen">
|
||||||
|
<QCard class="settings-dialog">
|
||||||
|
<QCardSection>
|
||||||
|
<div class="text-h6">Personal start.gg API token</div>
|
||||||
|
</QCardSection>
|
||||||
|
<QSeparator />
|
||||||
|
<QCardSection>
|
||||||
|
<div class="text-body2 q-mb-sm">
|
||||||
|
If OAuth fails, you can create a personal token manually:
|
||||||
|
</div>
|
||||||
|
<ol class="q-pl-md q-mb-md settings-token-steps">
|
||||||
|
<li>Go to https://start.gg/admin/profile/developer</li>
|
||||||
|
<li>Sign in with your account</li>
|
||||||
|
<li>From the 3 access tokens, click <strong>Third Party</strong></li>
|
||||||
|
<li>Create a new one and fill the description with any name you want</li>
|
||||||
|
<li>Copy the generated token and paste it below</li>
|
||||||
|
</ol>
|
||||||
|
<QInput
|
||||||
|
v-model="startggManualDraft"
|
||||||
|
label="Paste your personal token"
|
||||||
|
dense outlined type="password"
|
||||||
|
/>
|
||||||
|
</QCardSection>
|
||||||
|
<QSeparator />
|
||||||
|
<QCardActions align="right">
|
||||||
|
<QBtn flat no-caps label="Cancel" color="secondary" @click="isStartggManualDialogOpen = false" />
|
||||||
|
<QBtn flat no-caps color="negative" label="Delete token" @click="startggManualDraft = ''; saveStartggManualToken()" />
|
||||||
|
<QBtn no-caps color="primary" label="Save token" @click="saveStartggManualToken" />
|
||||||
|
</QCardActions>
|
||||||
|
</QCard>
|
||||||
|
</QDialog>
|
||||||
|
|
||||||
|
<!-- ── Diálogo token personal Challonge ─────────────────────────────────── -->
|
||||||
|
<QDialog v-model="isChallongeManualDialogOpen">
|
||||||
|
<QCard class="settings-dialog">
|
||||||
|
<QCardSection>
|
||||||
|
<div class="text-h6">Personal Challonge API token</div>
|
||||||
|
</QCardSection>
|
||||||
|
<QSeparator />
|
||||||
|
<QCardSection>
|
||||||
|
<div class="text-body2 q-mb-sm">
|
||||||
|
If OAuth fails, paste a personal Challonge API token.
|
||||||
|
</div>
|
||||||
|
<QInput
|
||||||
|
v-model="challongeManualDraft"
|
||||||
|
label="Paste your personal Challonge token"
|
||||||
|
dense outlined type="password"
|
||||||
|
/>
|
||||||
|
</QCardSection>
|
||||||
|
<QSeparator />
|
||||||
|
<QCardActions align="right">
|
||||||
|
<QBtn flat no-caps label="Cancel" color="secondary" @click="isChallongeManualDialogOpen = false" />
|
||||||
|
<QBtn flat no-caps color="negative" label="Delete token" @click="challongeManualDraft = ''; saveChallongeManualToken()" />
|
||||||
|
<QBtn no-caps color="primary" label="Save token" @click="saveChallongeManualToken" />
|
||||||
|
</QCardActions>
|
||||||
|
</QCard>
|
||||||
|
</QDialog>
|
||||||
|
|
||||||
</QPage>
|
</QPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.settings-layout {
|
||||||
|
max-width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-card {
|
.settings-card {
|
||||||
max-width: 600px;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dialog {
|
||||||
|
min-width: 320px;
|
||||||
|
width: min(560px, 90vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-token-steps {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fila de integración: logo | info | acciones */
|
||||||
|
.integration-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-row__logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-row__info {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-row__actions {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user