photoprism/frontend/src/component/auth/menu.vue
Ömer Duran 6dc0acac13
Settings: Reorganize UI and add an Accessibility section #848 #799 #5429
* Frontend: Add settings for albums, favorites, folders, and media features

* Frontend: Enhance download settings for files and albums #848

* Frontend: Enhance download settings for files and albums #848

* Frontend: Update album download settings to restrict access to super admins

* Frontend: Add album sorting options to settings for super admins

* Frontend: Update download settings visibility

* Frontend: Refine download settings access control for user roles

* Tests: Add album, folder, moment, state, and month order selection options to page-model

* Frontend: Update access control for super admins in settings to include scope check

* Tests: Add new tests for download name and album order settings persistence

* Tests: Implement new tests for default album sort order persistence in settings

* Frontend: Move default album order settings

* Frontend: Add zoom accessibility option in settings #799

* Frontend: Update download name from "Share Identifier" to "Share Friendly" and refine hints for download options in settings

* Tests: Add new acceptance test for disabling album downloads while allowing file downloads in settings

* Frontend: Improve download options layout and hints

* Frontend: Refactor download options condition in settings page

* Frontend: Implement native zoom control in lightbox component

* Settings: Add Collections tab and move Services to its own page #848 #5429

- Add a super-admin Collections tab after Content holding the album
  Download and Sort Order settings; remove those cards from Content.
- Move Services from a settings tab to a dedicated page
  (page/services.vue) with a search toolbar and a Reload / Learn More
  action menu; link it from the navigation settings menu.
- Implement server-side services search (acc_name filter) with tests.
- De-duplicate the picture sort-order options into a shared
  SortOrderOptions builder in options.js.
- Update acceptance/unit tests and page models to match.

* Frontend: Regenerate translation catalogs #848 #5429

Ran `make gettext-extract` to pick up the new settings strings
(Collections, Features, Disable Downloads, and related messages) so
Weblate can translate them. Existing translations are preserved.

* Frontend: Move lightbox viewport-zoom handling into the view helper

- Move the viewport pinch-zoom lock/restore (disableNativeZoom /
  restoreNativeZoom + savedViewportContent) from lightbox.vue onto the
  View singleton in common/view.js.
- Drive it passively from View.apply() via a disableViewportZoom flag in
  the PLightbox case, alongside hideScrollbar / disableScrolling /
  disableNavigationGestures; lightbox.vue no longer references zoom.
- Make save/restore idempotent and extract the viewport constants.
- Cover the helpers and the apply() wiring in view.test.js.

* Settings: Add Accessibility section to General with Reduce Motion #848 #5429

- Move the page-zoom checkbox out of the feature-flags grid into a
  dedicated Accessibility card (super-admin only) with four toggles:
  Open on Hover, Reduce Motion, Hide Scrollbar, and Allow Page Zoom.
- Add a UI.ReduceMotion flag (defaults false) that toggles an
  html.reduce-motion class via config.setReduceMotion and suppresses map
  fly-to animations non-destructively (util.mapAnimateDuration).
- Make Open on Hover apply live by reading shouldOpenOnHover() through a
  reactive computed in the action/auth/lightbox menus; Zoom and Hide
  Scrollbar reload since they are baked into the server-rendered HTML.
- Reword two feature-grid hints to the "pictures" convention and cover
  the new helpers with Go and Vitest tests.

* Frontend: Regenerate translation catalogs #848 #5429

Ran `make gettext-extract` to pick up the new Accessibility settings
strings (Accessibility, Allow Page Zoom, Open on Hover, Hide Scrollbar,
Reduce Motion, and their hints) and the reworded feature-grid hints.
Existing translations are preserved.

---------

Co-authored-by: Michael Mayer <michael@photoprism.app>
2026-07-08 14:11:36 +02:00

122 lines
4.4 KiB
Vue

<template>
<v-menu
v-model="open"
location="top"
offset="6"
:min-width="250"
:z-index="2500"
:open-on-hover="openOnHover"
class="p-action-menu action-menu nav-user-menu"
content-class="nav-user-menu__content"
@update:model-value="onToggle"
>
<template #activator="{ props }">
<div v-bind="props" class="nav-user-activator clickable" :title="$gettext('Account')">
<slot></slot>
</div>
</template>
<v-list slim nav density="compact" bg-color="navigation" class="action-menu__list">
<template v-if="instances.length > 0">
<v-list-item
v-for="instance in instances"
:key="instance.namespace"
:title="instance.title"
:subtitle="instance.path || undefined"
:nav="true"
class="action-menu__item"
@click="onSwitch(instance)"
>
<template #prepend>
<img :src="instance.icon" :alt="instance.title" class="nav-user-menu__icon" @error="onIconError(instance)" />
</template>
</v-list-item>
<v-divider class="my-1"></v-divider>
</template>
<v-list-item
v-if="$config.feature('account')"
prepend-icon="mdi-shield-account-variant"
:title="$gettext('Manage Account')"
class="action-menu__item action-account"
@click="$emit('account')"
></v-list-item>
<v-list-item prepend-icon="mdi-power" :title="$gettext('Sign Out')" class="action-menu__item action-logout" @click="$emit('logout')"></v-list-item>
</v-list>
</v-menu>
</template>
<script>
import { listReachableInstances, instancePath } from "common/instances";
// defaultIcon is shown when a peer has no recorded app icon, or its icon fails to
// load; it is the bundled PhotoPrism logo served at the origin root.
const defaultIcon = "/static/icons/logo.svg";
// PAuthMenu is the navigation avatar overlay menu: Manage Account, Sign Out, and —
// when more than one instance is reachable on a shared domain — a Switch Instance
// list. It emits "account" and "logout" for the host navigation to handle.
export default {
name: "PAuthMenu",
emits: ["logout", "account"],
data() {
return {
open: false,
instances: [],
};
},
computed: {
// openOnHover reactively reflects the user's menu open-on-hover preference so a settings
// change applies without a page reload (persistent menus keep no mount-time snapshot).
openOnHover() {
return this.$util.shouldOpenOnHover();
},
},
mounted() {
this.refresh();
},
methods: {
// refresh rescans shared localStorage for reachable peer instances, adding a
// display path (the SiteUrl's base path) for the menu subtitle.
refresh() {
const namespace = this.$config?.values?.storageNamespace;
this.instances = listReachableInstances({ currentNamespace: namespace }).map((instance) => {
// Only proxied instances (e.g. "/i/pro-1") get a path subtitle; a root
// ("/") or empty path means the Portal itself, so leave it blank.
const path = instancePath(instance.url);
return {
...instance,
path: path && path !== "/" ? path : "",
icon: instance.icon || defaultIcon,
};
});
},
// onToggle rescans peers each time the menu opens so the switcher stays current.
onToggle(open) {
if (open) {
this.refresh();
}
},
// onSwitch navigates the browser to the selected instance's app entry point
// (its frontend route, e.g. /i/pro-1/library or /portal) so the user lands
// in the app instead of a web-overlay landing page; its own login then completes
// the standard OIDC flow when a session is required. Falls back to the SiteUrl.
onSwitch(instance) {
const target = instance && (instance.route || instance.url);
if (target) {
this.navigate(target);
}
},
// navigate sends the browser to the given URL (a seam so onSwitch is testable).
navigate(url) {
window.location.assign(url);
},
// onIconError falls back to the default logo when an instance's recorded app
// icon fails to load (e.g. a removed asset or one served cross-origin). The
// guard avoids an error loop if the default logo itself fails.
onIconError(instance) {
if (instance.icon !== defaultIcon) {
instance.icon = defaultIcon;
}
},
},
};
</script>