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:
neon
2026-09-10 18:03:11 +00:00
parent 26712eb02a
commit d9ea13ba45
25 changed files with 347 additions and 30 deletions
+51
View File
@@ -0,0 +1,51 @@
# BookOrbit Additional Terms to the GNU AGPLv3
Effective 10 September 2026.
Copyright (C) 2025-2026 neon. Additional copyright is held by the respective BookOrbit contributors.
These non-permissive additional terms supplement the GNU Affero General Public License, version 3 only, as permitted by sections 7(b), 7(c), 7(d), and 7(e).
These terms apply to BookOrbit material owned by neon and distributed with this notice, and to contributions whose copyright holders expressly accept these terms. Other contributions remain subject to the license terms under which they were originally submitted.
## 1. Attribution
Every interactive user interface of a covered work containing BookOrbit material must provide a prominent and readily accessible **About** or **Legal Notices** feature displaying:
> **Powered by BookOrbit**
>
> Copyright (C) 2025-2026 neon.
>
> Additional copyright is held by the respective BookOrbit contributors.
>
> Original project: <https://github.com/bookorbit/bookorbit>
>
> 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.
"Powered by BookOrbit" must link directly to <https://github.com/bookorbit/bookorbit>.
The feature must be available to all users without administrative permission. It and the required notice must not be removed, disabled, hidden, obscured, or made materially less prominent than comparable About, help, or legal-notice features.
## 2. Source Code
When AGPLv3 section 13 applies, the same feature must offer users the complete Corresponding Source for the exact version being operated, at no charge. A link only to the original BookOrbit repository is not sufficient for a modified version.
## 3. Modified Versions
A modified version must prominently state:
> This is a modified version of BookOrbit and is not an official BookOrbit release.
It must provide a relevant modification date and must not claim or imply that it is an official, approved, sponsored, or endorsed BookOrbit release.
## 4. Trademarks
Except for the factual attribution required above, no trademark rights are granted. Without written permission, a modified version may not use "BookOrbit", a confusingly similar name, or a BookOrbit logo as its primary name or branding.
## 5. Preservation and Enforcement
Source distributions containing covered BookOrbit material must preserve this file, the GNU AGPLv3 license, applicable copyright notices, and notices identifying modifications and their dates.
Compliance with these terms is an express condition of permission to modify or convey covered BookOrbit material or make a modified version available for remote network interaction. Noncompliance is subject to section 8 of the AGPLv3.
These terms apply only to the extent permitted by section 7 of the AGPLv3. If any provision is unenforceable, the remaining provisions continue in effect.
+1 -1
View File
@@ -72,7 +72,7 @@ ENV PORT=3000
COPY --from=server-builder --chown=node:node /deploy ./
COPY --from=client-builder --chown=node:node /app/client/dist ./public
COPY --from=server-builder --chown=node:node /app/server/entrypoint.sh ./entrypoint.sh
COPY --chown=node:node LICENSE NOTICE ./
COPY --chown=node:node LICENSE NOTICE ADDITIONAL_TERMS.md ./
COPY --chown=node:node server/bin/kepubify/ ./bin/kepubify/
COPY --chown=node:node koreader-plugin/bookorbit.koplugin/ ./koreader-plugin/bookorbit.koplugin/
+17 -13
View File
@@ -1,21 +1,25 @@
BookOrbit Attribution Notice
BookOrbit Attribution and Legal Notice
Copyright (C) 2025-2026 neon and BookOrbit contributors.
Copyright (C) 2025-2026 neon.
Additional copyright is held by the respective BookOrbit contributors.
BookOrbit is free software licensed under the GNU Affero General Public
License, version 3. See LICENSE for the complete license terms.
License, version 3 only. See LICENSE for the complete license text.
Additional attribution term under GNU AGPL v3 section 7(b):
BookOrbit material owned by neon and contributions whose copyright holders
expressly accept the terms are also subject to the non-permissive additional
terms in ADDITIONAL_TERMS.md, as authorized by sections 7(b), 7(c), 7(d), and
7(e) of the GNU AGPL v3.
When a covered work containing BookOrbit material is conveyed, its source
distribution must preserve the following reasonable author attribution in a
top-level NOTICE, COPYRIGHT, README, or comparable legal notices file:
Other contributions remain subject to the license terms under which they were
originally submitted.
BookOrbit
Copyright (C) 2025-2026 neon and BookOrbit contributors.
The required attribution is:
Powered by BookOrbit
Copyright (C) 2025-2026 neon.
Additional copyright is held by the respective BookOrbit contributors.
Original project: https://github.com/bookorbit/bookorbit
This attribution does not grant permission to use the BookOrbit name or logo
to imply endorsement of a modified version. Modified versions that are
conveyed must identify themselves as modified, give relevant modification
dates, and otherwise comply with the GNU AGPL v3.
See ADDITIONAL_TERMS.md for the conditions governing display, source access,
modified versions, preservation, and trademarks.
+2 -2
View File
@@ -150,6 +150,6 @@ When adding user-facing text in code, add the Vue I18n key only to `client/src/l
## License and Attribution
BookOrbit is licensed under the **[GNU Affero General Public License v3.0](LICENSE)**.
BookOrbit is licensed under the **[GNU Affero General Public License v3.0 only](LICENSE)**.
Forks and modified distributions must preserve BookOrbit's reasonable author attribution as described in the **[BookOrbit Attribution Notice](NOTICE)**, in accordance with section 7(b) of the GNU AGPL v3.
BookOrbit material whose copyright holders have authorized them is also subject to the **[BookOrbit Additional Terms](ADDITIONAL_TERMS.md)** under sections 7(b), 7(c), 7(d), and 7(e) of the GNU AGPL v3. See the **[Attribution and Legal Notice](NOTICE)** for the required attribution.
+1 -1
View File
@@ -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",
+2
View File
@@ -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>
+11
View File
@@ -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>
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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>
+14
View File
@@ -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": {
+1 -1
View File
@@ -281,4 +281,4 @@ See [AI_POLICY.md](AI_POLICY.md). The short version: disclose all AI usage in yo
## License
By contributing, you agree that your contributions are licensed under the same license as the project (AGPL-3.0), including the attribution term in the [BookOrbit Attribution Notice](../NOTICE).
By submitting a contribution, you license it under the [GNU Affero General Public License, version 3 only](../LICENSE), together with the [BookOrbit Additional Terms](../ADDITIONAL_TERMS.md). You represent that you have the authority to grant this license. Copyright in your contribution remains yours.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "bookorbit",
"description": "Self-hosted library management and reading platform for ebooks, PDFs, audiobooks, and comics",
"author": "neonsolstice",
"license": "AGPL-3.0",
"license": "AGPL-3.0-only",
"homepage": "https://bookorbit.app",
"repository": {
"type": "git",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@bookorbit/plugin-api",
"version": "1.0.0",
"private": true,
"license": "AGPL-3.0",
"license": "AGPL-3.0-only",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"devDependencies": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@bookorbit/types",
"version": "0.0.1",
"private": true,
"license": "AGPL-3.0",
"license": "AGPL-3.0-only",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"devDependencies": {
+1 -1
View File
@@ -4,7 +4,7 @@
"description": "BookOrbit server - 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",