mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
78a177b1bc
* fix(orbit): harden the guest outbox — recover lost suggestions, avoid a duplicate outbox Two outbox robustness issues on the poll-based, non-atomic playlist transport: - Lost-update: a track a guest appends to its outbox can be wiped by a concurrent host sweep-clear before the host recorded it, leaving the suggestion lost AND stuck on "waiting on host" forever (it only clears once it reaches the host's play queue). The playlist read-modify-write can't be made atomic, so recover: re-send a pending suggestion the host hasn't recorded (absent from state.queue) past a grace window — the host dedupes by (user, trackId), so it's idempotent — and give up + toast past a 45s window so the row stops hanging. New pendingResend planner module with unit tests; suggest + tick share ensureTrackInOutbox. - Duplicate outbox: a transient getPlaylists failure at join was swallowed to [], so the existing-outbox check missed and a second outbox was created. Distinguish "lookup failed" from "genuinely absent" and retry once before falling back to create. New i18n key for the give-up toast across all locales. * fix(orbit): reject non-http(s) schemes in share invites normalizeShareServerUrl only prepends http:// when the server string doesn't already start with "http", so an invite carrying a scheme like httpx:// or https-phish:// slips through unchanged. Reject anything whose parsed protocol isn't http(s) at parse time, before the known-server match. Add round-trip tests covering the accepted and rejected schemes. * fix(orbit): read exit-modal role fresh to avoid a stale keydown closure The exit modal's Enter/Escape handler binds once per open (deps [isOpen]) and closed over the role prop, so a role change while the modal was up would act on a stale value. Read role from the store inside the action instead, and drop the now-unused reactive subscription. * docs(changelog): consolidate Orbit fixes into one block, add 1159
202 lines
12 KiB
TypeScript
202 lines
12 KiB
TypeScript
export const orbit = {
|
|
triggerLabel: 'Orbit',
|
|
triggerTooltip: 'Start eller bli med i en delt lytteøkt',
|
|
launchCreate: 'Opprett en økt',
|
|
launchJoin: 'Bli med i en økt',
|
|
launchHelp: 'Hvordan fungerer dette?',
|
|
launchHelpSoon: 'Kommer snart',
|
|
joinModalTitle: 'Bli med i en Orbit-økt',
|
|
joinModalSub: 'Lim inn invitasjonslenken verten sendte deg.',
|
|
joinModalLinkLabel: 'Invitasjonslenke',
|
|
joinModalLinkPlaceholder: 'psysonic2-…',
|
|
joinModalLinkHelper: 'Fungerer med enhver gyldig Orbit-lenke. Må peke til serveren du er innlogget på.',
|
|
joinModalPasteTooltip: 'Lim inn fra utklippstavlen',
|
|
joinModalSubmit: 'Bli med',
|
|
joinModalBusy: 'Blir med…',
|
|
joinErrEmpty: 'Lim inn en invitasjonslenke.',
|
|
joinErrInvalid: 'Dette ser ikke ut som en Orbit-invitasjonslenke.',
|
|
closeAria: 'Lukk',
|
|
heroTitlePrefix: 'Lytt sammen med',
|
|
heroTitleBrand: 'Orbit',
|
|
heroSub: 'Start en økt — andre brukere på {{server}} lytter med og kan foreslå spor.',
|
|
fallbackServer: 'denne serveren',
|
|
tipLan: 'Du er for øyeblikket koblet til via en hjemmenettverksadresse. Gjester utenfor nettverket kan ikke bli med — bytt til en offentlig serveradresse i innstillingene før du starter.',
|
|
tipRemote: 'For at gjester utenfor hjemmenettverket skal kunne bli med, må serveradressen være offentlig tilgjengelig. Hvis serveren din bare er intern, bytt før du starter.',
|
|
labelName: 'Øktnavn',
|
|
namePlaceholder: 'Velvet Orbit',
|
|
reshuffleTooltip: 'Nytt forslag',
|
|
reshuffleAria: 'Foreslå et nytt navn',
|
|
helperName: 'Slik vises økten for gjestene — behold den eller skriv din egen.',
|
|
labelMax: 'Maks gjester',
|
|
helperMax: 'Du telles ikke — dette begrenser bare innkommende gjester.',
|
|
labelLink: 'Invitasjonslenke',
|
|
tooltipCopied: 'Kopiert',
|
|
tooltipCopy: 'Kopier',
|
|
ariaCopyLink: 'Kopier invitasjonslenke',
|
|
helperLink: 'Send denne lenken til gjestene dine. De kan lime den inn hvor som helst i Psysonic med Ctrl+V.',
|
|
labelClearQueue: 'Tøm min kø først',
|
|
helperClearQueue: 'Start økten med en tom kø. Av: eksisterende spor beholdes og deles med gjester.',
|
|
btnCancel: 'Avbryt',
|
|
btnStarting: 'Starter…',
|
|
btnStart: 'Start Orbit',
|
|
btnCopyAndStart: 'Kopier lenke & start',
|
|
errNameRequired: 'Gi økten din et navn.',
|
|
errStartFailed: 'Kunne ikke starte.',
|
|
hostLabel: 'Vert: {{name}}',
|
|
participantsTooltip: 'Deltakere',
|
|
settingsTooltip: 'Øktinnstillinger',
|
|
shareTooltip: 'Del invitasjonslenke',
|
|
shuffleLabel: 'Kø stokkes på nytt om',
|
|
catchUpLabel: 'ta igjen',
|
|
catchUpTooltip: 'Hopp til vertens nåværende posisjon',
|
|
hostAway: 'Vert offline',
|
|
hostOnline: 'Vert online',
|
|
endTooltip: 'Avslutt økt',
|
|
leaveTooltip: 'Forlat økt',
|
|
helpTooltip: 'Hvordan Orbit fungerer',
|
|
diag: {
|
|
openTooltip: 'Diagnose — kopierbar hendelseslogg',
|
|
title: 'Orbit-diagnose',
|
|
role: 'Rolle',
|
|
hostTrack: 'Vertens spor-ID',
|
|
hostPos: 'Vertens posisjon',
|
|
guestTrack: 'Min spor-ID',
|
|
guestPos: 'Min posisjon',
|
|
drift: 'Avvik',
|
|
stateAge: 'Tilstandens alder',
|
|
eventLog: 'Hendelseslogg ({{count}})',
|
|
copyLabel: 'Kopier',
|
|
copyTooltip: 'Kopier loggen til utklippstavlen',
|
|
clearLabel: 'Tøm',
|
|
clearTooltip: 'Tøm bufferen',
|
|
empty: 'Ingen hendelser ennå — de dukker opp når Orbit synkroniserer.',
|
|
hint: 'Åpne dette før du reproduserer problemet, klikk Kopier og lim inn i feilrapporten.',
|
|
copied: '{{count}} linjer kopiert',
|
|
copyFailed: 'Kunne ikke kopiere til utklippstavlen',
|
|
cleared: 'Buffer tømt',
|
|
},
|
|
helpTitle: 'Hvordan Orbit fungerer',
|
|
helpIntro: 'Orbit gjør Psysonic til et delt lytterom. En vert velger musikken; gjester lytter synkronisert og kan foreslå spor.',
|
|
helpHostOnly: '(vert)',
|
|
helpSec1Title: 'Hva er Orbit?',
|
|
helpSec1Body: 'En "lytte sammen"-modus — verten driver avspillingen, gjester blir med. Alt går via spillelister på din egen Navidrome-server; ingen ekstern infrastruktur.',
|
|
helpSec2Title: 'Vert og gjester',
|
|
helpSec2Body: 'Verten kontrollerer avspillingen (spill / pause / hopp / søk) og bestemmer når økten avsluttes. Gjester følger vertens avspilling, kan foreslå spor og forlate eller bli med igjen når som helst. Bare verten kan utvise eller permanent utestenge en deltaker.',
|
|
helpSec2WarnHead: 'Viktig: separate kontoer.',
|
|
helpSec2WarnBody: 'Hver deltaker trenger sin egen Navidrome-konto. Hvis vert og gjest logger inn med samme bruker kolliderer innsendingene, og verten ser aldri gjestens forslag.',
|
|
helpSec3Title: 'Starte og invitere',
|
|
helpSec3Body: 'Klikk på "Orbit"-knappen → Opprett en økt → startmodalen viser en klar-til-kopiering invitasjonslenke og lar deg eventuelt tømme gjeldende kø. Del lenken som du vil. Mens en økt kjører, kopierer delknappen i øktlinjen øverst lenken når som helst.',
|
|
helpSec4Title: 'Bli med',
|
|
helpSec4Body: 'Lim inn invitasjonslenken hvor som helst i Psysonic med Ctrl+V / Cmd+V — bli-med-spørsmålet dukker opp automatisk. Alternativt: "Orbit" → Bli med i en økt → lim inn i feltet. Hvis lenken peker til en server du har konto på, bytter Psysonic for deg. Ved flere kontoer på den serveren spør en liten velger hvilken som skal brukes.',
|
|
helpSec5Title: 'Foreslå spor',
|
|
helpSec5Body: 'I enhver sporliste: dobbeltklikk en rad, eller høyreklikk → "Legg til i Orbit-økt". Et enkelt klikk viser et hint i stedet for å dumpe hele albumet i den delte køen — det er bevisst. Eksplisitte bulk-handlinger som "Spill alt" eller "Spill album" ber først om bekreftelse og legger deretter til (erstatter aldri).',
|
|
helpSec6Title: 'Den delte køen',
|
|
helpSec6Body: 'Køen viser vertens kommende spor — synlig for alle. Innkommende bulk-tillegg legges alltid til bakerst, så gjesteforslag går ikke tapt. Auto-stokking omorganiserer køen periodisk (konfigurerbar: 1 / 5 / 10 / 15 / 30 min). Hvis avspillingen din avviker fra verten, bringer "Ta igjen"-knappen i øktlinjen deg tilbake til vertens liveposisjon.',
|
|
helpSec7Title: 'Godkjenninger',
|
|
helpSec7Body: 'Som standard trenger gjesteforslag manuell godkjenning — de vises i en "Godkjenninger"-stripe øverst i køpanelet med godta / avvis-knapper. Auto-godkjenning kan slås på i øktinnstillingene slik at alt lander direkte.',
|
|
helpSec8Title: 'Deltakere og innstillinger',
|
|
helpSec8Body: 'Åpne innstillingsikonet i øktlinjen for stokkingsfrekvens, auto-godkjenning og "Stokk nå"-snarveien. Klikk på deltakerantallet for å se hvem som er koblet til — utvis (kan bli med igjen via lenken) eller utesteng (utelåst for økten). Gjester ser også deltakerlisten, men bare-lesing.',
|
|
helpSec9Title: 'Avslutte økten',
|
|
helpSec9Body: 'Vert X → bekreftelsesdialog → økten lukkes for alle og serverspillelistene ryddes opp automatisk. Gjest X → gjesten forlater, økten fortsetter. Hvis verten er stille i 5 minutter, forlater gjesten automatisk med en melding; korte gjentilkoblinger innenfor tiden er usynlige.',
|
|
participantsInviteLabel: 'Invitasjonslenke',
|
|
participantsCountLabel: '{{count}} i økten',
|
|
participantsHost: 'vert',
|
|
participantsEmpty: 'Ingen gjester ennå',
|
|
participantsKickTooltip: 'Fjern fra økten',
|
|
participantsKickAria: 'Fjern {{user}}',
|
|
participantsRemoveTooltip: 'Fjern fra økten',
|
|
participantsRemoveAria: 'Fjern {{user}} fra denne økten',
|
|
participantsBanTooltip: 'Uteste permanent',
|
|
participantsBanAria: 'Uteste {{user}} permanent',
|
|
participantsMuteTooltip: 'Blokker forslag',
|
|
participantsUnmuteTooltip: 'Tillat forslag',
|
|
participantsMuteAria: 'Blokker forslag fra {{user}}',
|
|
participantsUnmuteAria: 'Tillat forslag fra {{user}} igjen',
|
|
confirmRemoveTitle: 'Fjerne fra økten?',
|
|
confirmRemoveBody: '{{user}} fjernes fra økten, men kan bli med igjen via invitasjonslenken din.',
|
|
confirmRemoveConfirm: 'Fjern',
|
|
confirmBanTitle: 'Uteste permanent?',
|
|
confirmBanBody: '{{user}} fjernes og hindres i å bli med i denne økten igjen. Kan ikke angres i løpet av økten.',
|
|
confirmBanConfirm: 'Uteste',
|
|
confirmCancel: 'Avbryt',
|
|
confirmJoinTitle: 'Bli med i Orbit-økt?',
|
|
confirmJoinBody: '{{host}} inviterte deg til "{{name}}". Bli med i økten?',
|
|
confirmJoinConfirm: 'Bli med',
|
|
confirmLeaveTitle: 'Forlate økten?',
|
|
confirmLeaveBody: 'Forlate "{{name}}"? Du kan bli med igjen når som helst via {{host}}s invitasjonslenke.',
|
|
confirmLeaveConfirm: 'Forlat',
|
|
confirmEndTitle: 'Avslutte økten for alle?',
|
|
confirmEndBody: '"{{name}}" lukkes for alle deltakere. Øktens spillelister ryddes opp automatisk.',
|
|
confirmEndConfirm: 'Avslutt økt',
|
|
invalidLinkTitle: 'Invitasjonslenken er ikke lenger gyldig',
|
|
invalidLinkBody: 'Denne Orbit-økten finnes ikke lenger eller er allerede avsluttet. Be verten om en ny invitasjonslenke.',
|
|
settingsTitle: 'Øktinnstillinger',
|
|
settingAutoApprove: 'Godkjenn forslag automatisk',
|
|
settingAutoApproveHint: 'Gjesteforslag lander umiddelbart i køen din. Av: de blir værende i øktlisten for senere gjennomgang.',
|
|
settingAutoShuffle: 'Automatisk stokking',
|
|
settingAutoShuffleHint: 'Periodisk Fisher-Yates-stokking av kommende kø. Av: rekkefølgen endres bare når du omorganiserer den.',
|
|
settingShuffleInterval: 'Stokk på nytt hver',
|
|
settingShuffleIntervalHint: 'Hvor ofte kommende kø stokkes på nytt mens økten kjører.',
|
|
suggestBlockedMuted: 'Verten har blokkert deg for denne økten — forslag er av.',
|
|
settingShuffleIntervalValue_one: '{{count}} min',
|
|
settingShuffleIntervalValue_other: '{{count}} min',
|
|
settingShuffleNow: 'Stokk nå',
|
|
toastSuggested: '{{user}} foreslo "{{title}}"',
|
|
toastSuggestedMany: '{{count}} nye forslag i køen',
|
|
toastSuggestLost: 'Forslaget ditt nådde kanskje ikke verten',
|
|
toastShuffled: 'Kø stokket',
|
|
toastJoined: 'Ble med i økt',
|
|
toastLoginFirst: 'Logg inn før du blir med i en økt',
|
|
toastSwitchServer: 'Bytt til {{url}} først, og lim inn igjen',
|
|
toastNoAccountForServer: 'Du har ikke tilgang til {{url}}. Be verten om en invitasjon.',
|
|
toastSwitchFailed: 'Kunne ikke bytte til {{url}}',
|
|
accountPickerTitle: 'Hvilken konto?',
|
|
accountPickerSub: 'Du har mer enn én konto for {{url}}. Velg den du vil bli med i økten med.',
|
|
toastJoinFail: 'Kunne ikke bli med i økten',
|
|
joinErrNotFound: 'Økt ikke funnet',
|
|
joinErrEnded: 'Økten er avsluttet',
|
|
joinErrFull: 'Økten er full',
|
|
joinErrKicked: 'Du kan ikke bli med i denne økten igjen',
|
|
joinErrNoUser: 'Ingen aktiv server',
|
|
joinErrServerError: 'Kunne ikke bli med',
|
|
ctxAddToSession: 'Legg til i Orbit-økt',
|
|
ctxSuggestedToast: 'Foreslått til økten',
|
|
ctxSuggestFailed: 'Kunne ikke foreslå — ikke blitt med',
|
|
ctxAddToSessionHost: 'Legg til i Orbit-økt',
|
|
ctxAddedHostToast: 'Lagt til i økten',
|
|
ctxAddHostFailed: 'Kunne ikke legge til i økten',
|
|
bulkConfirmTitle: 'Legg alt til i Orbit-køen?',
|
|
bulkConfirmBody: 'Dette vil slippe {{count}} spor i den delte køen på én gang. Det er mye for gjestene dine. Fortsette?',
|
|
bulkConfirmYes: 'Legg til alle',
|
|
bulkConfirmNo: 'Avbryt',
|
|
guestLive: 'Live',
|
|
guestPlaying: 'Spiller nå',
|
|
guestPaused: 'Pauset',
|
|
guestLoading: 'Laster…',
|
|
guestSuggestions: 'Forslag',
|
|
guestUpNext: 'Neste',
|
|
guestUpNextEmpty: 'Ingenting i kø. Åpne et spors kontekstmeny og velg "Legg til i Orbit-økt" for å foreslå ett.',
|
|
guestPendingTitle: 'Venter på vert',
|
|
guestPendingHint: 'Foreslått — dukker opp i køen når verten tar det.',
|
|
approvalTitle: 'Venter på godkjenning',
|
|
approvalFrom: 'Foreslått av {{user}}',
|
|
approvalAccept: 'Godta',
|
|
approvalDecline: 'Avvis',
|
|
guestUpNextMore: '+ {{count}} flere i vertens kø',
|
|
guestSubmitter: 'av {{user}}',
|
|
queueAddedByHost: 'Lagt til av verten',
|
|
queueAddedByYou: 'Lagt til av deg',
|
|
queueAddedByUser: 'Lagt til av {{user}}',
|
|
guestEmpty: 'Ingen forslag ennå. Åpne et spors kontekstmeny og velg "Legg til i Orbit-økt".',
|
|
guestFooter: 'Verten kontrollerer avspillingen — du kan ikke endre listen.',
|
|
exitKickedTitle: 'Du ble utestengt fra økten',
|
|
exitRemovedTitle: 'Du ble fjernet fra økten',
|
|
exitEndedTitle: 'Verten avsluttet økten',
|
|
exitHostTimeoutTitle: 'Vert ikke tilgjengelig',
|
|
exitHostTimeoutBody: '{{host}} har ikke sendt en oppdatering på en stund, så "{{name}}" ble lukket på din side. Du kan bli med igjen når som helst med invitasjonslenken.',
|
|
exitKickedBody: '{{host}} utestengte deg fra "{{name}}". Du kan ikke bli med igjen.',
|
|
exitRemovedBody: '{{host}} fjernet deg fra "{{name}}". Du kan bli med igjen når som helst via invitasjonslenken.',
|
|
exitEndedBody: '"{{name}}" er avsluttet. Håper du hadde det gøy.',
|
|
exitOk: 'OK',
|
|
};
|