diff --git a/src/App.vue b/src/App.vue index f5c2e3d..764d3d9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,8 +15,6 @@ - - @@ -26,7 +24,6 @@ import NavigationIcons from "@/components/header/NavigationIcons.vue"; import Popup from "@/components/Popup.vue"; import CommandPalette from "@/components/ui/CommandPalette.vue"; - import DarkmodeToggle from "@/components/ui/DarkmodeToggle.vue"; const router = useRouter(); diff --git a/src/components/CastListItem.vue b/src/components/CastListItem.vue index 53f4754..60f1b47 100644 --- a/src/components/CastListItem.vue +++ b/src/components/CastListItem.vue @@ -1,9 +1,25 @@ @@ -33,85 +49,139 @@ return "/assets/no-image_small.svg"; }); + const metaText = computed(() => { + if ("character" in props.creditItem && props.creditItem.character) { + return props.creditItem.character; + } + if ("job" in props.creditItem && props.creditItem.job) { + return props.creditItem.job; + } + if ("year" in props.creditItem && props.creditItem.year) { + return props.creditItem.year; + } + return ""; + }); + + const imageAltText = computed(() => { + const name = props.creditItem.name || (props.creditItem as any).title || ""; + if ("character" in props.creditItem) { + return `${name} as ${props.creditItem.character}`; + } + if ("job" in props.creditItem) { + return `${name}, ${props.creditItem.job}`; + } + return name ? `Poster for ${name}` : "No image available"; + }); + + const ariaLabel = computed(() => { + const name = props.creditItem.name || (props.creditItem as any).title || ""; + if ("character" in props.creditItem && props.creditItem.character) { + return `View ${name}, played ${props.creditItem.character}`; + } + if ("job" in props.creditItem && props.creditItem.job) { + return `View ${name}, ${props.creditItem.job}`; + } + return `View ${name}`; + }); + function openCastItem() { store.dispatch("popup/open", { ...props.creditItem }); } - diff --git a/src/components/Popup.vue b/src/components/Popup.vue index d2c2e47..57afa0b 100644 --- a/src/components/Popup.vue +++ b/src/components/Popup.vue @@ -1,16 +1,26 @@ diff --git a/src/components/torrent/TorrentTable.vue b/src/components/torrent/TorrentTable.vue index 1981fa6..2ef0d26 100644 --- a/src/components/torrent/TorrentTable.vue +++ b/src/components/torrent/TorrentTable.vue @@ -1,58 +1,69 @@ diff --git a/src/components/ui/DarkmodeToggle.vue b/src/components/ui/DarkmodeToggle.vue deleted file mode 100644 index 9d3de1a..0000000 --- a/src/components/ui/DarkmodeToggle.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - - - diff --git a/src/components/ui/SeasonedButton.vue b/src/components/ui/SeasonedButton.vue index 2f82c50..b002955 100644 --- a/src/components/ui/SeasonedButton.vue +++ b/src/components/ui/SeasonedButton.vue @@ -2,7 +2,7 @@ @@ -15,7 +15,7 @@ } interface Emit { - (e: "click"); + (e: "click", event?: MouseEvent); } defineProps(); @@ -37,7 +37,6 @@ min-height: 45px; padding: 5px 10px 4px 10px; margin: 0; - margin-right: 0.3rem; color: $text-color; background: $background-color-secondary; cursor: pointer; diff --git a/src/components/ui/SeasonedInput.vue b/src/components/ui/SeasonedInput.vue index bba9a1c..9d32a1f 100644 --- a/src/components/ui/SeasonedInput.vue +++ b/src/components/ui/SeasonedInput.vue @@ -81,7 +81,6 @@ display: flex; width: 100%; position: relative; - max-width: 35rem; border: 1px solid var(--text-color-50); background-color: var(--background-color-secondary); diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts index 09856d1..17d87b0 100644 --- a/src/composables/useTheme.ts +++ b/src/composables/useTheme.ts @@ -22,9 +22,9 @@ function applyTheme(theme: Theme) { } export function useTheme() { - const savedTheme = computed(() => { - return (localStorage.getItem("theme-preference") as Theme) || "auto"; - }); + const savedTheme = computed( + () => (localStorage.getItem("theme-preference") as Theme) || "auto" + ); function initTheme() { const theme = savedTheme.value; diff --git a/src/modules/user.ts b/src/modules/user.ts index 2ac1bdd..206bae6 100644 --- a/src/modules/user.ts +++ b/src/modules/user.ts @@ -17,7 +17,7 @@ export interface CookieOptions { * Read a cookie value. */ export function getAuthorizationCookie(): string | null { - const key = 'authorization'; + const key = "authorization"; const array = document.cookie.split(";"); let match = null; diff --git a/src/pages/404Page.vue b/src/pages/404Page.vue index 2556b7a..679cb32 100644 --- a/src/pages/404Page.vue +++ b/src/pages/404Page.vue @@ -1,20 +1,28 @@ diff --git a/src/pages/GenPasswordPage.vue b/src/pages/GenPasswordPage.vue new file mode 100644 index 0000000..212aa2e --- /dev/null +++ b/src/pages/GenPasswordPage.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/pages/MissingPlexAuthPage.vue b/src/pages/MissingPlexAuthPage.vue new file mode 100644 index 0000000..55c72a9 --- /dev/null +++ b/src/pages/MissingPlexAuthPage.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/pages/RegisterPage.vue b/src/pages/RegisterPage.vue index b33061a..ea717e9 100644 --- a/src/pages/RegisterPage.vue +++ b/src/pages/RegisterPage.vue @@ -1,40 +1,107 @@ diff --git a/src/pages/SigninPage.vue b/src/pages/SigninPage.vue index ee84085..816ce48 100644 --- a/src/pages/SigninPage.vue +++ b/src/pages/SigninPage.vue @@ -1,31 +1,44 @@ diff --git a/src/pages/TorrentsPage.vue b/src/pages/TorrentsPage.vue index 561edc1..43e02f6 100644 --- a/src/pages/TorrentsPage.vue +++ b/src/pages/TorrentsPage.vue @@ -1,28 +1,25 @@ diff --git a/src/routes.ts b/src/routes.ts index 649392f..3bc48af 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -4,6 +4,7 @@ import type { RouteRecordRaw, RouteLocationNormalized } from "vue-router"; /* eslint-disable-next-line import-x/no-cycle */ import store from "./store"; import { usePlexAuth } from "./composables/usePlexAuth"; + const { getPlexAuthCookie } = usePlexAuth(); declare global { @@ -58,8 +59,7 @@ const routes: RouteRecordRaw[] = [ }, { name: "signin", - path: "/signin", - alias: "/login", + path: "/login", component: () => import("./pages/SigninPage.vue") }, { @@ -76,8 +76,18 @@ const routes: RouteRecordRaw[] = [ // } // }, { - name: "404", - path: "/404", + name: "password-gen", + path: "/password", + component: () => import("./pages/GenPasswordPage.vue") + }, + { + name: "missing-plex-auth", + path: "/missing/plex", + component: () => import("./pages/MissingPlexAuthPage.vue") + }, + { + path: "/:pathMatch(.*)*", + name: "NotFound", component: () => import("./pages/404Page.vue") } // { @@ -102,7 +112,7 @@ const hasPlexAccount = () => { // Check Vuex store first if (store.getters["user/plexUserId"] !== null) return true; - // Fallback to localStorage/cookie for page refreshes + // Fallback to localStorage const authToken = getPlexAuthCookie(); return !!authToken; }; @@ -120,15 +130,14 @@ router.beforeEach( // send user to signin page. if (to.matched.some(record => record.meta.requiresAuth)) { if (!loggedIn()) { - next({ path: "/signin" }); + next({ path: "/login" }); } } if (to.matched.some(record => record.meta.requiresPlexAccount)) { if (!hasPlexAccount()) { next({ - path: "/settings", - query: { missingPlexAccount: true } + path: "/missing/plex" }); } } diff --git a/src/scss/shared-auth.scss b/src/scss/shared-auth.scss new file mode 100644 index 0000000..e4d7318 --- /dev/null +++ b/src/scss/shared-auth.scss @@ -0,0 +1,100 @@ +// Shared styles for authentication pages (signin, register) +@import "variables"; +@import "media-queries"; + +// Base auth page layout +.auth-page { + padding: 3rem; + max-width: 100%; + + @include mobile-only { + padding: 0.75rem; + } +} + +.auth-content { + max-width: 600px; + + @include mobile-only { + max-width: 100%; + } + + &--wide { + max-width: 700px; + } +} + +.auth-header { + margin-bottom: 2.5rem; + + @include mobile-only { + margin-bottom: 2rem; + } +} + +.auth-title { + margin: 0 0 0.75rem 0; + font-size: 2.5rem; + font-weight: 600; + color: $text-color; + line-height: 1.2; + + @include mobile-only { + font-size: 2rem; + } +} + +.auth-subtitle { + margin: 0; + font-size: 1.1rem; + font-weight: 300; + color: var(--text-color-60); + line-height: 1.5; + + @include mobile-only { + font-size: 1rem; + } +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 1.25rem; + margin-bottom: 2rem; +} + +.auth-button { + margin-top: 0.5rem; + max-width: 200px; + + @include mobile-only { + max-width: 100%; + } +} + +.auth-footer { + padding-top: 2rem; + border-top: 1px solid var(--text-color-10); +} + +.auth-footer-text { + margin: 0; + font-size: 1rem; + color: var(--text-color-60); + + @include mobile-only { + font-size: 0.95rem; + } +} + +.auth-link { + color: var(--highlight-color); + text-decoration: none; + font-weight: 500; + transition: opacity 0.2s; + + &:hover { + opacity: 0.8; + text-decoration: underline; + } +} diff --git a/src/scss/variables.scss b/src/scss/variables.scss index b4dfdbe..e27cd91 100644 --- a/src/scss/variables.scss +++ b/src/scss/variables.scss @@ -114,10 +114,35 @@ $color-error: var(--color-error) !default; $color-error-highlight: var(--color-error-highlight) !default; .halloween { - --text-color: #6a318c; - --text-color-secondary: #fb5a33; - --background-color: #80c350; - --background-color-secondary: #ff9234; + --text-color: #f5f5f5; + --text-color-90: rgba(245, 245, 245, 0.9); + --text-color-70: rgba(245, 245, 245, 0.7); + --text-color-50: rgba(245, 245, 245, 0.5); + --text-color-10: rgba(245, 245, 245, 0.1); + --text-color-5: rgba(245, 245, 245, 0.05); + --text-color-secondary: #ff6600; + --background-color: #1a0e2e; + --background-color-secondary: #2d1b3d; + --background-ui: #3d2550; + --background-95: rgba(26, 14, 46, 0.95); + --background-90: rgba(26, 14, 46, 0.9); + --background-80: rgba(26, 14, 46, 0.8); + --background-70: rgba(45, 27, 61, 0.7); + --background-40: rgba(61, 37, 80, 0.4); + --background-0: rgba(26, 14, 46, 0); + --highlight-color: #ff6600; + --color-green: #ff6600; + --color-green-90: rgba(255, 102, 0, 0.9); + --color-green-70: rgba(255, 102, 0, 0.7); + --table-background-color: #0d0618; + --table-header-text-color: #ff6600; + --color-success: rgba(138, 43, 226, 0.8); + --color-success-text: #f5f5f5; + --color-success-highlight: #8a2be2; + --color-warning: rgba(255, 140, 0, 0.7); + --color-warning-highlight: #ff8c00; + --color-error: rgba(220, 20, 60, 0.8); + --color-error-highlight: #dc143c; } .dark { @@ -158,3 +183,98 @@ $color-error-highlight: var(--color-error-highlight) !default; --table-background-color: #081c24; --table-header-text-color: white; } + +.ocean { + --text-color: #e0f7ff; + --text-color-90: rgba(224, 247, 255, 0.9); + --text-color-70: rgba(224, 247, 255, 0.7); + --text-color-50: rgba(224, 247, 255, 0.5); + --text-color-10: rgba(224, 247, 255, 0.1); + --text-color-5: rgba(224, 247, 255, 0.05); + --text-color-secondary: #00d4ff; + --background-color: #0f2027; + --background-color-secondary: #203a43; + --background-ui: #2c5364; + --background-95: rgba(15, 32, 39, 0.95); + --background-90: rgba(15, 32, 39, 0.9); + --background-80: rgba(15, 32, 39, 0.8); + --background-70: rgba(32, 58, 67, 0.7); + --background-40: rgba(44, 83, 100, 0.4); + --background-0: rgba(15, 32, 39, 0); + --highlight-color: #00d4ff; + --color-green: #00d4ff; + --color-green-90: rgba(0, 212, 255, 0.9); + --color-green-70: rgba(0, 212, 255, 0.7); + --table-background-color: #0a1519; + --table-header-text-color: #00d4ff; +} + +.nordic { + --text-color: #2c3e2e; + --text-color-90: rgba(44, 62, 46, 0.9); + --text-color-70: rgba(44, 62, 46, 0.7); + --text-color-50: rgba(44, 62, 46, 0.5); + --text-color-10: rgba(44, 62, 46, 0.1); + --text-color-5: rgba(44, 62, 46, 0.05); + --text-color-secondary: #5a8a68; + --background-color: #f5f0e8; + --background-color-secondary: #fffef9; + --background-ui: #e8dfc8; + --background-95: rgba(245, 240, 232, 0.95); + --background-90: rgba(245, 240, 232, 0.9); + --background-80: rgba(245, 240, 232, 0.8); + --background-70: rgba(232, 223, 200, 0.7); + --background-40: rgba(232, 223, 200, 0.4); + --background-0: rgba(245, 240, 232, 0); + --highlight-color: #3d6e4e; + --color-green: #3d6e4e; + --color-green-90: rgba(61, 110, 78, 0.95); + --color-green-70: rgba(61, 110, 78, 0.7); + --table-background-color: #6d5a47; + --table-header-text-color: #fffef9; + --color-success: rgba(61, 110, 78, 0.85); + --color-success-text: #fffef9; + --color-success-highlight: #2d5e3e; + --color-warning: rgba(184, 134, 11, 0.75); + --color-warning-highlight: #d4a017; + --color-error: rgba(165, 42, 42, 0.85); + --color-error-highlight: #a52a2a; + --background-nav-logo: #2c3e2e; + --white: #fff; + --white-70: rgba(255, 255, 255, 0.7); +} + +.seed { + --text-color: #fcfcf7; + --text-color-90: rgba(252, 252, 247, 0.9); + --text-color-70: rgba(252, 252, 247, 0.7); + --text-color-50: rgba(252, 252, 247, 0.5); + --text-color-10: rgba(252, 252, 247, 0.1); + --text-color-5: rgba(252, 252, 247, 0.05); + --text-color-secondary: #e9f0ca; + --background-color: #1c3a13; + --background-color-secondary: #334e2b; + --background-ui: #45663c; + --background-95: rgba(28, 58, 19, 0.95); + --background-90: rgba(28, 58, 19, 0.9); + --background-80: rgba(28, 58, 19, 0.8); + --background-70: rgba(51, 78, 43, 0.7); + --background-40: rgba(69, 102, 60, 0.4); + --background-0: rgba(28, 58, 19, 0); + --highlight-color: #e9f0ca; + --color-green: #e9f0ca; + --color-green-90: rgba(233, 240, 202, 0.9); + --color-green-70: rgba(233, 240, 202, 0.7); + --table-background-color: #142f0c; + --table-header-text-color: #e9f0ca; + --color-success: rgba(208, 217, 185, 0.85); + --color-success-text: #1c3a13; + --color-success-highlight: #d0d9b9; + --color-warning: rgba(233, 240, 202, 0.75); + --color-warning-highlight: #e9f0ca; + --color-error: rgba(185, 99, 94, 0.85); + --color-error-highlight: #b9635e; + --background-nav-logo: #fcfcf7; + --white: #fcfcf7; + --white-70: rgba(252, 252, 247, 0.7); +} diff --git a/src/utils.ts b/src/utils.ts index 703037d..8d2af7b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -89,8 +89,9 @@ export function setUrlQueryParameter(parameter: string, value: string): void { const params = new URLSearchParams(); params.append(parameter, value); - const url = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : "" - }${window.location.pathname}${params.toString().length ? `?${params}` : ""}`; + const url = `${window.location.protocol}//${window.location.hostname}${ + window.location.port ? `:${window.location.port}` : "" + }${window.location.pathname}${params.toString().length ? `?${params}` : ""}`; window.history.pushState({}, "search", url); } @@ -141,5 +142,5 @@ export function formatBytes(bytes: number): string { const k = 1024; const sizes = ["Bytes", "KB", "MB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; + return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`; }