mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat: add BookOrbit attribution requirements
Define AGPLv3 additional terms and expose attribution, source, and license details through unobtrusive About surfaces.
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
||||
"description": "BookOrbit client - self-hosted library management and reading platform",
|
||||
"author": "neonsolstice",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"homepage": "https://bookorbit.app",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAuth } from '@/features/auth/composables/useAuth'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { resolveRouteViewKey } from '@/router/view-key'
|
||||
import LegalNotices from '@/components/legal/LegalNotices.vue'
|
||||
|
||||
const { isOpen } = useChangePasswordDialog()
|
||||
const themeStore = useThemeStore()
|
||||
@@ -53,6 +54,7 @@ provide(
|
||||
</router-view>
|
||||
<ChangePasswordDialog v-if="isOpen" />
|
||||
<WhatsNewDialog v-if="popupOpen" />
|
||||
<LegalNotices />
|
||||
<Toaster rich-colors position="bottom-right" :visible-toasts="5" :gap="8" />
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ExternalLink,
|
||||
Sparkles,
|
||||
Languages,
|
||||
Info,
|
||||
} from '@lucide/vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -57,6 +58,7 @@ import { DEFAULT_FORMAT_PRIORITY, LOCALE_LABELS, Permission, type Locale } from
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useLocaleStore } from '@/stores/locale'
|
||||
import { getFormatColor } from '@/features/book/lib/format-colors'
|
||||
import { useLegalNotices } from '@/components/legal/useLegalNotices'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -69,6 +71,7 @@ const { subscribe: subscribeNotifications } = useNotifications()
|
||||
const { hasUnseen: hasUnseenWhatsNew } = useWhatsNew()
|
||||
const themeStore = useThemeStore()
|
||||
const localeStore = useLocaleStore()
|
||||
const { openLegalNotices } = useLegalNotices()
|
||||
const currentLanguageLabel = computed(() => LOCALE_LABELS[localeStore.locale])
|
||||
const documentationUrl = 'https://bookorbit.app/what-is-bookorbit'
|
||||
|
||||
@@ -669,6 +672,10 @@ function formatBadgeStyle(fmt: string) {
|
||||
<ExternalLink :size="12" class="ml-auto text-muted-foreground" />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="openLegalNotices">
|
||||
<Info :size="15" class="mr-2 text-muted-foreground" />
|
||||
{{ t('components.legalNotices.about') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -742,6 +749,10 @@ function formatBadgeStyle(fmt: string) {
|
||||
{{ t('components.appHeader.whatsNew') }}
|
||||
<span v-if="hasUnseenWhatsNew" class="ml-auto h-1.5 w-1.5 rounded-full bg-primary" :aria-label="t('components.appHeader.new')" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="openLegalNotices">
|
||||
<Info :size="14" class="mr-2 text-muted-foreground" />
|
||||
{{ t('components.legalNotices.about') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<TooltipContent>{{ t('components.appHeader.help') }}</TooltipContent>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import LegalNotices from './LegalNotices.vue'
|
||||
import PublicLegalNotices from './PublicLegalNotices.vue'
|
||||
import { useLegalNotices } from './useLegalNotices'
|
||||
|
||||
vi.mock('reka-ui', () => ({
|
||||
DialogRoot: { props: ['open'], template: '<div v-if="open"><slot /></div>' },
|
||||
DialogPortal: { template: '<div><slot /></div>' },
|
||||
DialogOverlay: { template: '<div />' },
|
||||
DialogContent: { template: '<section class="legal-dialog"><slot /></section>' },
|
||||
DialogTitle: { template: '<h2><slot /></h2>' },
|
||||
DialogDescription: { template: '<p><slot /></p>' },
|
||||
}))
|
||||
|
||||
describe('LegalNotices', () => {
|
||||
beforeEach(() => {
|
||||
useLegalNotices().closeLegalNotices()
|
||||
})
|
||||
|
||||
it('keeps the legal notice available without authentication or elevated permission', async () => {
|
||||
const wrapper = mount(LegalNotices)
|
||||
|
||||
useLegalNotices().openLegalNotices()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.get('.legal-dialog').text()).toContain('Powered by BookOrbit')
|
||||
expect(wrapper.get('.legal-dialog').text()).toContain('Copyright (C) 2025-2026 neon.')
|
||||
expect(wrapper.get('.legal-dialog').text()).toContain('Additional copyright is held by the respective BookOrbit contributors.')
|
||||
expect(wrapper.get('.legal-dialog').text()).toContain('provided without warranty')
|
||||
})
|
||||
|
||||
it('links attribution, source, license, and additional terms to the project', async () => {
|
||||
const wrapper = mount(LegalNotices)
|
||||
useLegalNotices().openLegalNotices()
|
||||
await nextTick()
|
||||
|
||||
const links = wrapper.findAll('a')
|
||||
expect(links.map((link) => link.attributes('href'))).toEqual([
|
||||
'https://github.com/bookorbit/bookorbit',
|
||||
'https://github.com/bookorbit/bookorbit',
|
||||
'https://github.com/bookorbit/bookorbit/blob/main/LICENSE',
|
||||
'https://github.com/bookorbit/bookorbit/blob/main/ADDITIONAL_TERMS.md',
|
||||
])
|
||||
})
|
||||
|
||||
it('uses an inline linked attribution on public pages', async () => {
|
||||
const wrapper = mount(PublicLegalNotices)
|
||||
|
||||
const attribution = wrapper.get('a')
|
||||
expect(attribution.text()).toBe('Powered by BookOrbit')
|
||||
expect(attribution.attributes('href')).toBe('https://github.com/bookorbit/bookorbit')
|
||||
expect(wrapper.classes()).not.toContain('fixed')
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(useLegalNotices().open.value).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { ExternalLink, X } from '@lucide/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
|
||||
import { useLegalNotices } from './useLegalNotices'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { open, closeLegalNotices, handleOpenChange } = useLegalNotices()
|
||||
|
||||
const PROJECT_URL = 'https://github.com/bookorbit/bookorbit'
|
||||
const LICENSE_URL = `${PROJECT_URL}/blob/main/LICENSE`
|
||||
const ADDITIONAL_TERMS_URL = `${PROJECT_URL}/blob/main/ADDITIONAL_TERMS.md`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="open" @update:open="handleOpenChange">
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 z-50 bg-foreground/50" />
|
||||
<DialogContent
|
||||
aria-modal="true"
|
||||
class="fixed left-1/2 top-1/2 z-50 max-h-[calc(100svh-2rem)] w-[calc(100%-2rem)] max-w-xl -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border border-border bg-card p-6 shadow-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<DialogTitle class="text-lg font-semibold text-foreground">
|
||||
{{ t('components.legalNotices.title') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription class="mt-1 text-sm text-muted-foreground">
|
||||
{{ t('components.legalNotices.description') }}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-foreground outline-hidden hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:aria-label="t('common.close')"
|
||||
@click="closeLegalNotices"
|
||||
>
|
||||
<X :size="16" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4 text-sm">
|
||||
<a
|
||||
:href="PROJECT_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1.5 font-semibold text-primary underline-offset-4 hover:underline focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{{ t('components.legalNotices.poweredBy') }}
|
||||
<ExternalLink :size="14" aria-hidden="true" />
|
||||
</a>
|
||||
|
||||
<div class="space-y-1 text-foreground">
|
||||
<p>{{ t('components.legalNotices.originalDevelopment') }}</p>
|
||||
<p>{{ t('components.legalNotices.copyright') }}</p>
|
||||
<p>{{ t('components.legalNotices.contributors') }}</p>
|
||||
</div>
|
||||
|
||||
<p class="text-foreground">{{ t('components.legalNotices.licenseSummary') }}</p>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-3">
|
||||
<a
|
||||
:href="PROJECT_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-2 font-medium text-foreground outline-hidden hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{{ t('components.legalNotices.sourceCode') }}
|
||||
<ExternalLink :size="14" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
:href="LICENSE_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-2 font-medium text-foreground outline-hidden hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{{ t('components.legalNotices.license') }}
|
||||
<ExternalLink :size="14" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
:href="ADDITIONAL_TERMS_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-2 font-medium text-foreground outline-hidden hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{{ t('components.legalNotices.additionalTerms') }}
|
||||
<ExternalLink :size="14" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useLegalNotices } from './useLegalNotices'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { openLegalNotices } = useLegalNotices()
|
||||
|
||||
const PROJECT_URL = 'https://github.com/bookorbit/bookorbit'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative z-10 flex items-center justify-center gap-1.5 text-[11px] text-foreground">
|
||||
<a
|
||||
:href="PROJECT_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="rounded-sm outline-hidden underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{{ t('components.legalNotices.poweredBy') }}
|
||||
</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-sm outline-hidden underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
||||
@click="openLegalNotices"
|
||||
>
|
||||
{{ t('components.legalNotices.trigger') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
export function useLegalNotices() {
|
||||
function openLegalNotices() {
|
||||
open.value = true
|
||||
}
|
||||
|
||||
function closeLegalNotices() {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function handleOpenChange(value: boolean) {
|
||||
open.value = value
|
||||
}
|
||||
|
||||
return { open, openLegalNotices, closeLegalNotices, handleOpenChange }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Moon, Sun, Wallpaper } from '@lucide/vue'
|
||||
import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme'
|
||||
import { api } from '@/lib/api'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const themeStore = useThemeStore()
|
||||
@@ -75,7 +76,7 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-bg min-h-screen flex items-center justify-center px-4 overflow-hidden">
|
||||
<div class="login-bg min-h-screen flex flex-col items-center justify-center gap-3 px-4 overflow-hidden">
|
||||
<!-- Compact theme picker -->
|
||||
<div class="fixed bottom-5 right-5 z-20 flex items-center gap-1.5">
|
||||
<!-- Dark / light toggle -->
|
||||
@@ -238,6 +239,7 @@ async function handleSubmit() {
|
||||
<RouterLink to="/login" class="text-primary hover:underline">{{ t('auth.backToSignIn') }}</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
<PublicLegalNotices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useOidc } from './composables/useOidc'
|
||||
import { useSetupStatus } from './composables/useSetupStatus'
|
||||
import { useLoginOptions } from './composables/useLoginOptions'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const themeStore = useThemeStore()
|
||||
@@ -125,7 +126,7 @@ async function handleOidcLogin(provider: OidcProviderPublic) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-bg min-h-screen flex items-center justify-center px-4 overflow-hidden">
|
||||
<div class="login-bg min-h-screen flex flex-col items-center justify-center gap-3 px-4 overflow-hidden">
|
||||
<!-- Compact theme picker -->
|
||||
<div class="fixed top-5 right-5 z-20 flex items-center gap-1.5">
|
||||
<!-- Dark / light toggle -->
|
||||
@@ -334,6 +335,7 @@ async function handleOidcLogin(provider: OidcProviderPublic) {
|
||||
<RouterLink to="/register" class="text-primary hover:underline">{{ t('auth.login.signUp') }}</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
<PublicLegalNotices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Loader2, AlertCircle } from '@lucide/vue'
|
||||
import { useAuth } from '@/features/auth/composables/useAuth'
|
||||
import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -41,7 +42,7 @@ function goToLogin() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-background px-4">
|
||||
<div class="min-h-screen flex flex-col items-center justify-center gap-3 bg-background px-4">
|
||||
<div class="w-full max-w-sm text-center">
|
||||
<div v-if="status === 'loading'" class="space-y-4">
|
||||
<Loader2 :size="32" class="mx-auto text-primary animate-spin" />
|
||||
@@ -64,5 +65,6 @@ function goToLogin() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PublicLegalNotices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'
|
||||
import type { OidcCallbackResponse } from '@bookorbit/types'
|
||||
import { OidcErrorCode } from '@bookorbit/types'
|
||||
import { setAccessToken } from '@/lib/api'
|
||||
import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue'
|
||||
import { useAuth } from './composables/useAuth'
|
||||
import { useOidc, OidcLoginError } from './composables/useOidc'
|
||||
|
||||
@@ -82,7 +83,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-background px-4">
|
||||
<div class="min-h-screen flex flex-col items-center justify-center gap-3 bg-background px-4">
|
||||
<div class="w-full max-w-sm text-center">
|
||||
<h1 class="text-2xl font-serif font-semibold text-foreground mb-6">Book<span class="text-primary"> Orbit</span></h1>
|
||||
|
||||
@@ -98,5 +99,6 @@ onMounted(async () => {
|
||||
<RouterLink to="/login" class="text-sm text-primary hover:underline">{{ t('auth.oidc.tryAgain') }}</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<PublicLegalNotices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { Moon, Sun, Wallpaper } from '@lucide/vue'
|
||||
import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue'
|
||||
import { RegisterError, useAuth } from './composables/useAuth'
|
||||
import { checkIdentityText } from './lib/identity-text'
|
||||
|
||||
@@ -110,7 +111,7 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-bg min-h-screen flex items-center justify-center px-4 overflow-hidden">
|
||||
<div class="login-bg min-h-screen flex flex-col items-center justify-center gap-3 px-4 overflow-hidden">
|
||||
<!-- Compact theme picker -->
|
||||
<div class="fixed bottom-5 right-5 z-20 flex items-center gap-1.5">
|
||||
<!-- Dark / light toggle -->
|
||||
@@ -327,6 +328,7 @@ async function handleSubmit() {
|
||||
</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
<PublicLegalNotices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { Moon, Sun, Wallpaper } from '@lucide/vue'
|
||||
import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const themeStore = useThemeStore()
|
||||
@@ -95,7 +96,7 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-bg min-h-screen flex items-center justify-center px-4 overflow-hidden">
|
||||
<div class="login-bg min-h-screen flex flex-col items-center justify-center gap-3 px-4 overflow-hidden">
|
||||
<!-- Compact theme picker -->
|
||||
<div class="fixed bottom-5 right-5 z-20 flex items-center gap-1.5">
|
||||
<!-- Dark / light toggle -->
|
||||
@@ -267,6 +268,7 @@ async function handleSubmit() {
|
||||
<RouterLink to="/login" class="text-primary hover:underline">{{ t('auth.backToSignIn') }}</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
<PublicLegalNotices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { Moon, Sun, Wallpaper } from '@lucide/vue'
|
||||
import { ACCENT_OPTIONS, ACCENT_ROWS, RADIUS_OPTIONS, BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import PublicLegalNotices from '@/components/legal/PublicLegalNotices.vue'
|
||||
import { useAuth } from './composables/useAuth'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -85,7 +86,7 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-bg min-h-screen flex items-center justify-center px-4 overflow-hidden">
|
||||
<div class="login-bg min-h-screen flex flex-col items-center justify-center gap-3 px-4 overflow-hidden">
|
||||
<!-- Compact theme picker -->
|
||||
<div class="fixed bottom-5 right-5 z-20 flex items-center gap-1.5">
|
||||
<!-- Dark / light toggle -->
|
||||
@@ -305,6 +306,7 @@ async function handleSubmit() {
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<PublicLegalNotices />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Keyboard, X } from '@lucide/vue'
|
||||
import { Info, Keyboard, X } from '@lucide/vue'
|
||||
import { useLegalNotices } from '@/components/legal/useLegalNotices'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { openLegalNotices } = useLegalNotices()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
@@ -51,6 +53,11 @@ function handleBackdropClick(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function openAboutBookOrbit() {
|
||||
emit('close')
|
||||
openLegalNotices()
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', onKeyDown, true))
|
||||
onUnmounted(() => document.removeEventListener('keydown', onKeyDown, true))
|
||||
</script>
|
||||
@@ -87,6 +94,17 @@ onUnmounted(() => document.removeEventListener('keydown', onKeyDown, true))
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end border-t border-border/80 px-4 py-2.5">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-foreground outline-hidden hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
||||
@click="openAboutBookOrbit"
|
||||
>
|
||||
<Info :size="13" aria-hidden="true" />
|
||||
{{ t('components.legalNotices.about') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7266,6 +7266,20 @@
|
||||
"librariesEmptyHint": "Create a library to start adding books.",
|
||||
"smartScopesEmptyHint": "Smart scopes save a set of filters you use often.",
|
||||
"collectionsEmptyHint": "Collections group books you pick by hand."
|
||||
},
|
||||
"legalNotices": {
|
||||
"about": "About BookOrbit",
|
||||
"trigger": "Legal notices",
|
||||
"title": "About BookOrbit",
|
||||
"description": "BookOrbit attribution, licensing, and source code.",
|
||||
"poweredBy": "Powered by BookOrbit",
|
||||
"originalDevelopment": "This software contains material originally developed as BookOrbit.",
|
||||
"copyright": "Copyright (C) 2025-2026 neon.",
|
||||
"contributors": "Additional copyright is held by the respective BookOrbit contributors.",
|
||||
"licenseSummary": "Licensed under the GNU Affero General Public License, version 3 only, with additional terms. This software is provided without warranty. You may redistribute and modify it under those terms.",
|
||||
"sourceCode": "Source code",
|
||||
"license": "AGPLv3 license",
|
||||
"additionalTerms": "Additional terms"
|
||||
}
|
||||
},
|
||||
"customIcons": {
|
||||
|
||||
Reference in New Issue
Block a user