mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-04-24 16:53:37 +00:00
* include credentials on login fetch requests, allows set header response * Add theme composable and utility improvements - Create useTheme composable for centralized theme management - Update main.ts to use useTheme for initialization - Generalize getCookie utility in user module - Add utility functions for data formatting * Add Plex integration composables and icons - Create usePlexAuth composable for Plex OAuth flow - Create usePlexApi composable for Plex API interactions - Create useRandomWords composable for password generation - Add Plex-related icons (IconPlex, IconServer, IconSync) - Add Plex helper utilities - Update API with Plex-related endpoints * Add storage management components for data & privacy section - Create StorageManager component for browser storage overview - Create StorageSectionBrowser for localStorage/sessionStorage/cookies - Create StorageSectionServer for server-side data (mock) - Create ExportSection for data export functionality - Refactor DataExport component with modular sections - Add storage icons (IconCookie, IconDatabase, IconTimer) - Implement collapsible sections with visual indicators - Add colored borders per storage type - Display item counts and total size in headers * Add theme, password, and security settings components - Create ThemePreferences with visual theme selector - Create PasswordGenerator with passphrase and random modes - Create SecuritySettings wrapper for password management - Update ChangePassword to work with new layout - Implement improved slider UX with visual feedback - Add theme preview cards with gradients - Standardize component styling and typography * Add Plex settings and authentication components - Create PlexSettings component for Plex account management - Create PlexAuthButton with improved OAuth flow - Create PlexServerInfo for server details display - Use icon components instead of inline SVGs - Add sync and unlink functionality - Implement user-friendly authentication flow * Redesign settings page with two-column layout and ProfileHero - Create ProfileHero component with avatar and user info - Create RequestHistory component for Plex requests (placeholder) - Redesign SettingsPage with modern two-column grid layout - Add shared-settings.scss for consistent styling - Organize sections: Appearance, Security, Integrations, Data & Privacy - Implement responsive mobile layout - Standardize typography (h2: 1.5rem, 700 weight) - Add compact modifier for tighter sections
598 lines
13 KiB
Vue
598 lines
13 KiB
Vue
<template>
|
|
<div class="password-generator">
|
|
<div class="generator-panel">
|
|
<div class="generator-tabs">
|
|
<button
|
|
:class="['tab', { 'tab--active': mode === 'words' }]"
|
|
@click="mode = 'words'"
|
|
>
|
|
Passphrase
|
|
</button>
|
|
<button
|
|
:class="['tab', { 'tab--active': mode === 'chars' }]"
|
|
@click="mode = 'chars'"
|
|
>
|
|
Random
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="mode === 'words'" class="generator-content">
|
|
<div class="generator-header">
|
|
<h4>Passphrase Generator</h4>
|
|
<p>Create a memorable password using random words</p>
|
|
</div>
|
|
|
|
<div class="password-display" @click="copyPassword">
|
|
<span class="password-text password-text--mono">{{
|
|
generatedPassword
|
|
}}</span>
|
|
<button
|
|
class="copy-btn"
|
|
:title="copied ? 'Copied!' : 'Click to copy'"
|
|
>
|
|
{{ copied ? "✓" : "📋" }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="generator-options">
|
|
<div class="option-row">
|
|
<div class="slider-header">
|
|
<label>Words</label>
|
|
<span class="slider-value">{{ wordCount }}</span>
|
|
</div>
|
|
<input
|
|
v-model.number="wordCount"
|
|
type="range"
|
|
min="3"
|
|
max="7"
|
|
class="slider"
|
|
@input="generateWordsPassword"
|
|
/>
|
|
<div class="slider-labels">
|
|
<span>3</span>
|
|
<span>7</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="generator-content">
|
|
<div class="generator-header">
|
|
<h4>Random Password Generator</h4>
|
|
<p>Generate a secure random password</p>
|
|
</div>
|
|
|
|
<div class="password-display" @click="copyPassword">
|
|
<span class="password-text password-text--mono">{{
|
|
generatedPassword
|
|
}}</span>
|
|
<button
|
|
class="copy-btn"
|
|
:title="copied ? 'Copied!' : 'Click to copy'"
|
|
>
|
|
{{ copied ? "✓" : "📋" }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="generator-options">
|
|
<div class="option-row">
|
|
<div class="slider-header">
|
|
<label>Length</label>
|
|
<span class="slider-value">{{ charLength }}</span>
|
|
</div>
|
|
<input
|
|
v-model.number="charLength"
|
|
type="range"
|
|
min="12"
|
|
max="46"
|
|
class="slider"
|
|
@input="generateCharsPassword"
|
|
/>
|
|
<div class="slider-labels">
|
|
<span>12</span>
|
|
<span>46</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="option-row checkbox-row">
|
|
<label>
|
|
<input
|
|
v-model="includeUppercase"
|
|
type="checkbox"
|
|
@change="generateCharsPassword"
|
|
/>
|
|
Uppercase (A-Z)
|
|
</label>
|
|
<label>
|
|
<input
|
|
v-model="includeLowercase"
|
|
type="checkbox"
|
|
@change="generateCharsPassword"
|
|
/>
|
|
Lowercase (a-z)
|
|
</label>
|
|
<label>
|
|
<input
|
|
v-model="includeNumbers"
|
|
type="checkbox"
|
|
@change="generateCharsPassword"
|
|
/>
|
|
Numbers (0-9)
|
|
</label>
|
|
<label>
|
|
<input
|
|
v-model="includeSymbols"
|
|
type="checkbox"
|
|
@change="generateCharsPassword"
|
|
/>
|
|
Symbols (!@#$)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="generator-actions">
|
|
<button class="action-btn action-btn--secondary" @click="regenerate">
|
|
<IconActivity class="btn-icon" />
|
|
Regenerate
|
|
</button>
|
|
<button class="action-btn action-btn--primary" @click="usePassword">
|
|
Use This Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch, onMounted } from "vue";
|
|
import IconActivity from "@/icons/IconActivity.vue";
|
|
import { useRandomWords } from "@/composables/useRandomWords";
|
|
|
|
interface Emit {
|
|
(e: "passwordGenerated", password: string): void;
|
|
}
|
|
|
|
const emit = defineEmits<Emit>();
|
|
|
|
const mode = ref<"words" | "chars">("words");
|
|
const generatedPassword = ref("");
|
|
const copied = ref(false);
|
|
|
|
// Words mode options
|
|
const wordCount = ref(4);
|
|
const separator = ref("-");
|
|
|
|
// Chars mode options
|
|
const charLength = ref(16);
|
|
const includeUppercase = ref(true);
|
|
const includeLowercase = ref(true);
|
|
const includeNumbers = ref(true);
|
|
const includeSymbols = ref(true);
|
|
|
|
const { getRandomWords } = useRandomWords();
|
|
|
|
async function generateWordsPassword() {
|
|
const words = await getRandomWords(wordCount.value);
|
|
const password = words.join(separator.value);
|
|
generatedPassword.value = password;
|
|
}
|
|
|
|
function generateCharsPassword() {
|
|
let charset = "";
|
|
|
|
if (includeUppercase.value) charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
if (includeLowercase.value) charset += "abcdefghijklmnopqrstuvwxyz";
|
|
if (includeNumbers.value) charset += "0123456789";
|
|
if (includeSymbols.value) charset += "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
|
|
if (charset === "") charset = "abcdefghijklmnopqrstuvwxyz";
|
|
|
|
let password = "";
|
|
for (let i = 0; i < charLength.value; i++) {
|
|
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
}
|
|
|
|
generatedPassword.value = password;
|
|
}
|
|
|
|
async function regenerate() {
|
|
if (mode.value === "words") {
|
|
await generateWordsPassword();
|
|
} else {
|
|
generateCharsPassword();
|
|
}
|
|
}
|
|
|
|
async function copyPassword() {
|
|
try {
|
|
await navigator.clipboard.writeText(generatedPassword.value);
|
|
copied.value = true;
|
|
setTimeout(() => {
|
|
copied.value = false;
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error("Failed to copy:", err);
|
|
}
|
|
}
|
|
|
|
function usePassword() {
|
|
emit("passwordGenerated", generatedPassword.value);
|
|
// TODO: emit
|
|
// showGenerator.value = false;
|
|
}
|
|
|
|
watch(mode, async () => {
|
|
if (mode.value === "words") {
|
|
await generateWordsPassword();
|
|
} else {
|
|
generateCharsPassword();
|
|
}
|
|
});
|
|
|
|
onMounted(generateWordsPassword);
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import "scss/variables";
|
|
@import "scss/media-queries";
|
|
|
|
.password-generator {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.generator-panel {
|
|
margin-top: 0.75rem;
|
|
padding: 1rem;
|
|
background-color: var(--background-ui);
|
|
border-radius: 0.5rem;
|
|
border: 1px solid var(--background-40);
|
|
|
|
@include mobile-only {
|
|
padding: 0.75rem;
|
|
}
|
|
}
|
|
|
|
.generator-tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.tab {
|
|
flex: 1;
|
|
padding: 0.65rem 1rem;
|
|
background-color: transparent;
|
|
border: 1px solid var(--background-40);
|
|
border-radius: 0.25rem;
|
|
color: $text-color-70;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.9rem;
|
|
|
|
&:hover {
|
|
background-color: var(--background-40);
|
|
}
|
|
|
|
&--active {
|
|
background-color: var(--highlight-color);
|
|
border-color: var(--highlight-color);
|
|
color: $white;
|
|
}
|
|
}
|
|
|
|
.generator-content {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.generator-header {
|
|
margin-bottom: 0.75rem;
|
|
|
|
h4 {
|
|
margin: 0 0 0.15rem 0;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
color: $text-color;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
p {
|
|
margin: 0;
|
|
font-size: 0.8rem;
|
|
color: $text-color-70;
|
|
line-height: 1.3;
|
|
}
|
|
}
|
|
|
|
.password-display {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
background-color: var(--background-color);
|
|
border: 2px solid var(--highlight-color);
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 0.75rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
background-color: var(--background-40);
|
|
}
|
|
|
|
@include mobile-only {
|
|
padding: 0.6rem;
|
|
}
|
|
}
|
|
|
|
.password-text {
|
|
flex: 1;
|
|
font-size: 1.8rem;
|
|
font-weight: 500;
|
|
color: var(--highlight-color);
|
|
user-select: all;
|
|
word-break: break-all;
|
|
word-break: break-word;
|
|
-webkit-hyphens: auto;
|
|
hyphens: auto;
|
|
|
|
@include mobile-only {
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
&--mono {
|
|
font-family: "Courier New", monospace;
|
|
|
|
@include mobile-only {
|
|
font-size: 0.85rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.copy-btn {
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.25rem;
|
|
cursor: pointer;
|
|
padding: 0.25rem;
|
|
transition: transform 0.2s;
|
|
|
|
&:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
|
|
.generator-options {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.option-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
|
|
label {
|
|
font-size: 0.95rem;
|
|
color: $text-color;
|
|
font-weight: 600;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
&.checkbox-row {
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
|
|
@include mobile-only {
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
font-weight: 400;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
|
|
input[type="checkbox"] {
|
|
cursor: pointer;
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.slider-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.slider-value {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--highlight-color);
|
|
min-width: 2.5rem;
|
|
text-align: right;
|
|
}
|
|
|
|
.slider-labels {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 0.75rem;
|
|
color: $text-color-50;
|
|
margin-top: 0.25rem;
|
|
padding: 0 0.25rem;
|
|
}
|
|
|
|
.slider {
|
|
width: 100%;
|
|
height: 10px;
|
|
border-radius: 5px;
|
|
background: var(--background-40);
|
|
outline: none;
|
|
appearance: none;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
margin: 0.5rem 0;
|
|
|
|
@include mobile-only {
|
|
height: 12px;
|
|
}
|
|
|
|
&:hover {
|
|
background: var(--background-40);
|
|
}
|
|
|
|
&::-webkit-slider-thumb {
|
|
appearance: none;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
background: var(--highlight-color);
|
|
cursor: grab;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
transition: all 0.2s;
|
|
margin-top: -7px;
|
|
|
|
@include mobile-only {
|
|
width: 28px;
|
|
height: 28px;
|
|
margin-top: -8px;
|
|
}
|
|
|
|
&:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
&:active {
|
|
cursor: grabbing;
|
|
transform: scale(1.05);
|
|
}
|
|
}
|
|
|
|
&::-moz-range-thumb {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
background: var(--highlight-color);
|
|
cursor: grab;
|
|
border: none;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
transition: all 0.2s;
|
|
|
|
@include mobile-only {
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
|
|
&:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
&:active {
|
|
cursor: grabbing;
|
|
transform: scale(1.05);
|
|
}
|
|
}
|
|
|
|
&::-webkit-slider-runnable-track {
|
|
height: 10px;
|
|
border-radius: 5px;
|
|
|
|
@include mobile-only {
|
|
height: 12px;
|
|
}
|
|
}
|
|
|
|
&::-moz-range-track {
|
|
height: 10px;
|
|
border-radius: 5px;
|
|
background: var(--background-40);
|
|
|
|
@include mobile-only {
|
|
height: 12px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.separator-input {
|
|
padding: 0.5rem 0.75rem;
|
|
border: 1px solid var(--background-40);
|
|
border-radius: 0.25rem;
|
|
background-color: var(--background-color);
|
|
color: $text-color;
|
|
font-size: 0.85rem;
|
|
font-family: "Courier New", monospace;
|
|
text-align: center;
|
|
|
|
&:focus {
|
|
outline: none;
|
|
border-color: var(--highlight-color);
|
|
}
|
|
|
|
&::placeholder {
|
|
color: $text-color-50;
|
|
font-family: inherit;
|
|
}
|
|
}
|
|
|
|
.generator-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
|
|
@include mobile-only {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
}
|
|
|
|
.action-btn {
|
|
flex: 1;
|
|
padding: 0.6rem 1rem;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.4rem;
|
|
|
|
&--secondary {
|
|
background-color: var(--background-color);
|
|
color: $text-color;
|
|
border: 1px solid var(--background-40);
|
|
|
|
&:hover {
|
|
background-color: var(--background-40);
|
|
}
|
|
}
|
|
|
|
&--primary {
|
|
background-color: var(--highlight-color);
|
|
color: $white;
|
|
|
|
&:hover {
|
|
background-color: var(--color-green-90);
|
|
}
|
|
}
|
|
}
|
|
|
|
.btn-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
fill: currentColor;
|
|
}
|
|
</style>
|