diff --git a/lang/en/accounts.php b/lang/en/accounts.php index d3f0d5ee..f6686db6 100644 --- a/lang/en/accounts.php +++ b/lang/en/accounts.php @@ -125,6 +125,8 @@ 'title_success' => 'Connected', 'title_error' => 'Error', 'closing' => 'This window will close automatically...', + 'manual_close' => 'You can close this window.', + 'popup_blocked' => 'Could not open the connection window. Please allow popups and try again.', 'connected' => 'Account connected!', 'reconnected' => 'Account reconnected!', 'error_connecting' => 'Error connecting account. Please try again.', diff --git a/lang/en/assets.php b/lang/en/assets.php index a4b61bbe..57483f4a 100644 --- a/lang/en/assets.php +++ b/lang/en/assets.php @@ -11,8 +11,9 @@ 'upload' => [ 'drag_drop' => 'Drag & drop your files here, or click to select', - 'formats' => 'JPEG, PNG, GIF, WebP, MP4', + 'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF', 'uploading' => 'Uploading...', + 'failed' => 'Could not upload :file. Please try again.', ], 'empty' => [ diff --git a/lang/es/accounts.php b/lang/es/accounts.php index e665a3e9..987805c7 100644 --- a/lang/es/accounts.php +++ b/lang/es/accounts.php @@ -125,6 +125,8 @@ 'title_success' => 'Conectado', 'title_error' => 'Error', 'closing' => 'Esta ventana se cerrará automáticamente...', + 'manual_close' => 'Puedes cerrar esta ventana.', + 'popup_blocked' => 'No se pudo abrir la ventana de conexión. Permite las ventanas emergentes e inténtalo de nuevo.', 'connected' => '¡Cuenta conectada!', 'reconnected' => '¡Cuenta reconectada!', 'error_connecting' => 'Error al conectar la cuenta. Inténtalo de nuevo.', diff --git a/lang/es/assets.php b/lang/es/assets.php index d8c33dba..0945d737 100644 --- a/lang/es/assets.php +++ b/lang/es/assets.php @@ -13,8 +13,9 @@ 'upload' => [ 'drag_drop' => 'Arrastra y suelta tus archivos aquí o haz clic para seleccionar', - 'formats' => 'JPEG, PNG, GIF, WebP, MP4', + 'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF', 'uploading' => 'Subiendo...', + 'failed' => 'No se pudo subir :file. Inténtalo de nuevo.', ], 'empty' => [ diff --git a/lang/pt-BR/accounts.php b/lang/pt-BR/accounts.php index dee22d18..662cf78b 100644 --- a/lang/pt-BR/accounts.php +++ b/lang/pt-BR/accounts.php @@ -125,6 +125,8 @@ 'title_success' => 'Conectado', 'title_error' => 'Erro', 'closing' => 'Esta janela será fechada automaticamente...', + 'manual_close' => 'Você pode fechar esta janela.', + 'popup_blocked' => 'Não foi possível abrir a janela de conexão. Permita pop-ups e tente novamente.', 'connected' => 'Conta conectada!', 'reconnected' => 'Conta reconectada!', 'error_connecting' => 'Erro ao conectar conta. Por favor, tente novamente.', diff --git a/lang/pt-BR/assets.php b/lang/pt-BR/assets.php index ac24bd34..5da24132 100644 --- a/lang/pt-BR/assets.php +++ b/lang/pt-BR/assets.php @@ -13,8 +13,9 @@ 'upload' => [ 'drag_drop' => 'Arraste e solte seus arquivos aqui ou clique para selecionar', - 'formats' => 'JPEG, PNG, GIF, WebP, MP4', + 'formats' => 'JPEG, PNG, GIF, WebP, MP4, PDF', 'uploading' => 'Enviando...', + 'failed' => 'Não foi possível enviar :file. Tente novamente.', ], 'empty' => [ diff --git a/resources/js/components/MediaDropzone.vue b/resources/js/components/MediaDropzone.vue deleted file mode 100644 index 6960f23f..00000000 --- a/resources/js/components/MediaDropzone.vue +++ /dev/null @@ -1,313 +0,0 @@ - - - diff --git a/resources/js/components/accounts/NetworkConnectGrid.vue b/resources/js/components/accounts/NetworkConnectGrid.vue index 7e916b04..0960a355 100644 --- a/resources/js/components/accounts/NetworkConnectGrid.vue +++ b/resources/js/components/accounts/NetworkConnectGrid.vue @@ -3,6 +3,7 @@ import { router } from '@inertiajs/vue3'; import { IconAlertTriangle, IconCheck, IconPlus } from '@tabler/icons-vue'; import { trans } from 'laravel-vue-i18n'; import { computed, ref } from 'vue'; +import { toast } from 'vue-sonner'; import TelegramConnectDialog from '@/components/accounts/TelegramConnectDialog.vue'; import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue'; @@ -153,8 +154,14 @@ const disconnectModal = ref | null>( null, ); -const { openOAuthPopup } = useOAuthPopup(() => { - router.reload(); +const { openOAuthPopup } = useOAuthPopup((result) => { + if (result.success) { + toast.success(result.message); + router.reload(); + return; + } + + toast.error(result.message); }); const disconnectAccount = (account: ConnectedAccount) => { diff --git a/resources/js/components/assets/GalleryBrowser.vue b/resources/js/components/assets/GalleryBrowser.vue index ba603dd5..8399688e 100644 --- a/resources/js/components/assets/GalleryBrowser.vue +++ b/resources/js/components/assets/GalleryBrowser.vue @@ -249,7 +249,7 @@ const uploadFiles = async (files: File[]) => { collection: 'assets', }); } catch { - // ignore individual failure + toast.error(trans('assets.upload.failed', { file: file.name })); } } uploading.value = false; @@ -662,7 +662,7 @@ onUnmounted(() => { :key="asset.id" class="group relative overflow-hidden rounded-xl border-2 border-foreground bg-muted shadow-2xs transition-all hover:-translate-y-0.5 hover:shadow-md" :class="[ - (isPicker || !isVideo(asset)) ? 'cursor-pointer' : '', + 'cursor-pointer', isPicker && isSelected(asset.id) ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' : '', ]" @click="handleAssetClick(asset)" diff --git a/resources/js/composables/useDragAndDrop.ts b/resources/js/composables/useDragAndDrop.ts deleted file mode 100644 index e520ea0c..00000000 --- a/resources/js/composables/useDragAndDrop.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ref } from 'vue'; - -import { fromMimeType, MediaType } from '@/lib/mediaType'; - -export interface DragAndDropOptions { - acceptImages: boolean; - acceptVideos: boolean; - maxFiles: number; - disabled?: boolean; -} - -export function useDragAndDrop(options: DragAndDropOptions) { - const isDragging = ref(false); - - const isValidFileType = (file: File): boolean => { - const type = fromMimeType(file.type); - - if (type === MediaType.Image && !options.acceptImages) { - return false; - } - if (type === MediaType.Video && !options.acceptVideos) { - return false; - } - return type === MediaType.Image || type === MediaType.Video; - }; - - const handleDragOver = (e: DragEvent) => { - e.preventDefault(); - if (!options.disabled) { - isDragging.value = true; - } - }; - - const handleDragLeave = (e: DragEvent) => { - e.preventDefault(); - isDragging.value = false; - }; - - const handleDrop = (e: DragEvent, currentMediaCount: number, onUpload: (files: File[]) => void) => { - e.preventDefault(); - isDragging.value = false; - - if (options.disabled || !e.dataTransfer?.files) { - return; - } - - const files = Array.from(e.dataTransfer.files); - const validFiles = files.filter(isValidFileType); - - if (validFiles.length === 0) { - return; - } - - // Limit to remaining slots - const remainingSlots = options.maxFiles - currentMediaCount; - const filesToUpload = validFiles.slice(0, remainingSlots); - - if (filesToUpload.length > 0) { - onUpload(filesToUpload); - } - }; - - return { - isDragging, - handleDragOver, - handleDragLeave, - handleDrop, - }; -} diff --git a/resources/js/composables/useOAuthPopup.ts b/resources/js/composables/useOAuthPopup.ts index b042ef7e..c0a62778 100644 --- a/resources/js/composables/useOAuthPopup.ts +++ b/resources/js/composables/useOAuthPopup.ts @@ -1,4 +1,6 @@ +import { trans } from 'laravel-vue-i18n'; import { onMounted, onUnmounted } from 'vue'; +import { toast } from 'vue-sonner'; import { connect as blueskyConnect } from '@/routes/app/social/bluesky'; import { connect as discordConnect } from '@/routes/app/social/discord'; @@ -24,7 +26,6 @@ const CONNECT_ROUTES: Record string }> = { [Platform.Instagram]: instagramConnect, [Platform.InstagramFacebook]: instagramFacebookConnect, [Platform.LinkedIn]: linkedinConnect, - [Platform.LinkedInPage]: linkedinConnect, [Platform.Mastodon]: mastodonConnect, [Platform.Pinterest]: pinterestConnect, [Platform.Threads]: threadsConnect, @@ -33,12 +34,18 @@ const CONNECT_ROUTES: Record string }> = { [Platform.YouTube]: youtubeConnect, }; +export interface SocialOAuthResult { + success: boolean; + message: string; + platform: string | null; +} + /** * Opens a platform's OAuth connect flow in a centered popup and invokes - * `onSuccess` when the popup posts back the `social-oauth-callback` message. - * The listener is wired to the calling component's lifecycle. + * `onResult` with the popup's `{success, message}` outcome. The listener is + * wired to the calling component's lifecycle. */ -export const useOAuthPopup = (onSuccess: () => void) => { +export const useOAuthPopup = (onResult: (result: SocialOAuthResult) => void) => { const openOAuthPopup = (platform: string) => { const route = CONNECT_ROUTES[platform]; @@ -49,18 +56,26 @@ export const useOAuthPopup = (onSuccess: () => void) => { const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2; const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2; - window.open( + const popup = window.open( route.url(), 'oauth-popup', `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},scrollbars=yes,resizable=yes`, ); + + if (!popup) { + toast.error(trans('accounts.popup_callback.popup_blocked')); + } }; const handleMessage = (event: MessageEvent) => { if (event.origin !== window.location.origin) return; if (event.data?.type !== 'social-oauth-callback') return; - onSuccess(); + onResult({ + success: Boolean(event.data.success), + message: String(event.data.message ?? ''), + platform: event.data.platform ?? null, + }); }; onMounted(() => window.addEventListener('message', handleMessage)); diff --git a/resources/js/date.ts b/resources/js/date.ts index fb91ad2c..cab538cc 100644 --- a/resources/js/date.ts +++ b/resources/js/date.ts @@ -243,12 +243,12 @@ export default { /** * Format a duration in milliseconds (e.g. "—", "500ms", "1.5s", "2m 30s"). */ - formatDurationMs(ms: number | null): string { - if (ms === null) { + formatDurationMs(ms: number | null | undefined): string { + if (ms == null) { return '—'; } if (ms < 1000) { - return `${ms}ms`; + return `${Math.round(ms)}ms`; } const seconds = ms / 1000; diff --git a/resources/js/pages/accounts/FacebookPageSelect.vue b/resources/js/pages/accounts/FacebookPageSelect.vue index 85513db3..7ef36d2d 100644 --- a/resources/js/pages/accounts/FacebookPageSelect.vue +++ b/resources/js/pages/accounts/FacebookPageSelect.vue @@ -12,7 +12,6 @@ interface Page { name: string; username: string | null; picture: string | null; - access_token: string; } interface Workspace { diff --git a/resources/js/pages/accounts/PopupCallback.vue b/resources/js/pages/accounts/PopupCallback.vue index d44c8f04..98522418 100644 --- a/resources/js/pages/accounts/PopupCallback.vue +++ b/resources/js/pages/accounts/PopupCallback.vue @@ -1,6 +1,6 @@