mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-15 00:33:44 +08:00
feat: Add password protection for settings and remove Docker publish workflow.
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
name: Docker Build and Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from package.json
|
||||
id: package-version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: kuekhaoyang/kvideo
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=v${{ steps.package-version.outputs.version }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=v${{ steps.package-version.outputs.version }}
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ steps.package-version.outputs.version }}
|
||||
|
||||
- name: Debug - List files
|
||||
run: |
|
||||
echo "=== Files in current directory ==="
|
||||
ls -la
|
||||
echo "=== Node modules status ==="
|
||||
if [ -d "node_modules" ]; then echo "node_modules exists"; else echo "node_modules NOT found"; fi
|
||||
echo "=== Lockfiles ==="
|
||||
ls -la | grep -E "lock|yarn" || echo "No lockfiles found"
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
+4
-1
@@ -4,6 +4,7 @@ import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { ServiceWorkerRegister } from "@/components/ServiceWorkerRegister";
|
||||
import { PasswordGate } from "@/components/PasswordGate";
|
||||
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -36,7 +37,9 @@ export default function RootLayout({
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
<PasswordGate>
|
||||
{children}
|
||||
</PasswordGate>
|
||||
<Analytics />
|
||||
<ServiceWorkerRegister />
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -12,15 +12,27 @@ export function useSettingsPage() {
|
||||
const [isRestoreDefaultsDialogOpen, setIsRestoreDefaultsDialogOpen] = useState(false);
|
||||
const [editingSource, setEditingSource] = useState<VideoSource | null>(null);
|
||||
|
||||
const [passwordAccess, setPasswordAccess] = useState(false);
|
||||
const [accessPasswords, setAccessPasswords] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const settings = settingsStore.getSettings();
|
||||
setSources(settings.sources || []);
|
||||
setSortBy(settings.sortBy);
|
||||
setPasswordAccess(settings.passwordAccess);
|
||||
setAccessPasswords(settings.accessPasswords);
|
||||
}, []);
|
||||
|
||||
const handleSourcesChange = (newSources: VideoSource[]) => {
|
||||
setSources(newSources);
|
||||
settingsStore.saveSettings({ sources: newSources, sortBy, searchHistory: true, watchHistory: true });
|
||||
settingsStore.saveSettings({
|
||||
sources: newSources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess,
|
||||
accessPasswords
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddSource = (source: VideoSource) => {
|
||||
@@ -39,7 +51,52 @@ export function useSettingsPage() {
|
||||
|
||||
const handleSortChange = (newSort: SortOption) => {
|
||||
setSortBy(newSort);
|
||||
settingsStore.saveSettings({ sources, sortBy: newSort, searchHistory: true, watchHistory: true });
|
||||
settingsStore.saveSettings({
|
||||
sources,
|
||||
sortBy: newSort,
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess,
|
||||
accessPasswords
|
||||
});
|
||||
};
|
||||
|
||||
const handlePasswordToggle = (enabled: boolean) => {
|
||||
setPasswordAccess(enabled);
|
||||
settingsStore.saveSettings({
|
||||
sources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess: enabled,
|
||||
accessPasswords
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddPassword = (password: string) => {
|
||||
const updated = [...accessPasswords, password];
|
||||
setAccessPasswords(updated);
|
||||
settingsStore.saveSettings({
|
||||
sources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess,
|
||||
accessPasswords: updated
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemovePassword = (password: string) => {
|
||||
const updated = accessPasswords.filter(p => p !== password);
|
||||
setAccessPasswords(updated);
|
||||
settingsStore.saveSettings({
|
||||
sources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess,
|
||||
accessPasswords: updated
|
||||
});
|
||||
};
|
||||
|
||||
const handleExport = (includeSearchHistory: boolean, includeWatchHistory: boolean) => {
|
||||
@@ -59,6 +116,8 @@ export function useSettingsPage() {
|
||||
const settings = settingsStore.getSettings();
|
||||
setSources(settings.sources);
|
||||
setSortBy(settings.sortBy);
|
||||
setPasswordAccess(settings.passwordAccess);
|
||||
setAccessPasswords(settings.accessPasswords);
|
||||
}
|
||||
return success;
|
||||
};
|
||||
@@ -78,6 +137,8 @@ export function useSettingsPage() {
|
||||
return {
|
||||
sources,
|
||||
sortBy,
|
||||
passwordAccess,
|
||||
accessPasswords,
|
||||
isAddModalOpen,
|
||||
isExportModalOpen,
|
||||
isImportModalOpen,
|
||||
@@ -92,6 +153,9 @@ export function useSettingsPage() {
|
||||
handleSourcesChange,
|
||||
handleAddSource,
|
||||
handleSortChange,
|
||||
handlePasswordToggle,
|
||||
handleAddPassword,
|
||||
handleRemovePassword,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleRestoreDefaults,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
import { SourceSettings } from '@/components/settings/SourceSettings';
|
||||
import { SortSettings } from '@/components/settings/SortSettings';
|
||||
import { DataSettings } from '@/components/settings/DataSettings';
|
||||
import { PasswordSettings } from '@/components/settings/PasswordSettings';
|
||||
import { SettingsHeader } from '@/components/settings/SettingsHeader';
|
||||
import { useSettingsPage } from './hooks/useSettingsPage';
|
||||
|
||||
@@ -15,6 +16,8 @@ export default function SettingsPage() {
|
||||
const {
|
||||
sources,
|
||||
sortBy,
|
||||
passwordAccess,
|
||||
accessPasswords,
|
||||
isAddModalOpen,
|
||||
isExportModalOpen,
|
||||
isImportModalOpen,
|
||||
@@ -28,6 +31,9 @@ export default function SettingsPage() {
|
||||
handleSourcesChange,
|
||||
handleAddSource,
|
||||
handleSortChange,
|
||||
handlePasswordToggle,
|
||||
handleAddPassword,
|
||||
handleRemovePassword,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleRestoreDefaults,
|
||||
@@ -43,6 +49,15 @@ export default function SettingsPage() {
|
||||
{/* Header */}
|
||||
<SettingsHeader />
|
||||
|
||||
{/* Password Settings */}
|
||||
<PasswordSettings
|
||||
enabled={passwordAccess}
|
||||
passwords={accessPasswords}
|
||||
onToggle={handlePasswordToggle}
|
||||
onAdd={handleAddPassword}
|
||||
onRemove={handleRemovePassword}
|
||||
/>
|
||||
|
||||
{/* Source Management */}
|
||||
<SourceSettings
|
||||
sources={sources}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
import { Lock } from 'lucide-react';
|
||||
|
||||
const SESSION_UNLOCKED_KEY = 'kvideo-unlocked';
|
||||
|
||||
export function PasswordGate({ children }: { children: React.ReactNode }) {
|
||||
const [isLocked, setIsLocked] = useState(true);
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState(false);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
checkLockStatus();
|
||||
}, []);
|
||||
|
||||
const checkLockStatus = () => {
|
||||
const settings = settingsStore.getSettings();
|
||||
if (!settings.passwordAccess) {
|
||||
setIsLocked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true';
|
||||
if (isUnlocked) {
|
||||
setIsLocked(false);
|
||||
} else {
|
||||
setIsLocked(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlock = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const settings = settingsStore.getSettings();
|
||||
if (settings.accessPasswords.includes(password)) {
|
||||
sessionStorage.setItem(SESSION_UNLOCKED_KEY, 'true');
|
||||
setIsLocked(false);
|
||||
setError(false);
|
||||
} else {
|
||||
setError(true);
|
||||
// Shake animation trigger
|
||||
const form = document.getElementById('password-form');
|
||||
form?.classList.add('animate-shake');
|
||||
setTimeout(() => form?.classList.remove('animate-shake'), 500);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isClient) return null; // Prevent hydration mismatch
|
||||
|
||||
if (!isLocked) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-[var(--bg-color)] bg-[image:var(--bg-image)] text-[var(--text-color)]">
|
||||
<div className="w-full max-w-md p-4">
|
||||
<form
|
||||
id="password-form"
|
||||
onSubmit={handleUnlock}
|
||||
className="bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] p-8 shadow-[var(--shadow-md)] flex flex-col items-center gap-6 transition-all duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1)"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-[var(--radius-full)] bg-[var(--accent-color)]/10 flex items-center justify-center text-[var(--accent-color)] mb-2 shadow-[var(--shadow-sm)] border border-[var(--glass-border)]">
|
||||
<Lock size={32} />
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-2xl font-bold">访问受限</h2>
|
||||
<p className="text-[var(--text-color-secondary)]">请输入访问密码以继续</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-4">
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
setError(false);
|
||||
}}
|
||||
placeholder="输入密码..."
|
||||
className={`w-full px-4 py-3 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border ${error ? 'border-red-500' : 'border-[var(--glass-border)]'
|
||||
} focus:outline-none focus:border-[var(--accent-color)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent-color)_30%,transparent)] transition-all duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1) text-[var(--text-color)] placeholder-[var(--text-color-secondary)]`}
|
||||
autoFocus
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-sm text-red-500 text-center animate-pulse">
|
||||
密码错误
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 px-4 bg-[var(--accent-color)] text-white font-bold rounded-[var(--radius-2xl)] hover:translate-y-[-2px] hover:brightness-110 shadow-[var(--shadow-sm)] hover:shadow-[0_4px_8px_var(--shadow-color)] active:translate-y-0 active:scale-[0.98] transition-all duration-200"
|
||||
>
|
||||
解锁访问
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<style jsx global>{`
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
.animate-shake {
|
||||
animation: shake 0.3s cubic-bezier(.36,.07,.19,.97) both;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { Trash2, Plus, Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
interface PasswordSettingsProps {
|
||||
enabled: boolean;
|
||||
passwords: string[];
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onAdd: (password: string) => void;
|
||||
onRemove: (password: string) => void;
|
||||
}
|
||||
|
||||
export function PasswordSettings({
|
||||
enabled,
|
||||
passwords,
|
||||
onToggle,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}: PasswordSettingsProps) {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleAdd = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newPassword) return;
|
||||
|
||||
if (passwords.includes(newPassword)) {
|
||||
setError('密码已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
onAdd(newPassword);
|
||||
setNewPassword('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection title="访问控制" description="为应用启用密码保护功能。">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-[var(--text-color)]">
|
||||
启用密码访问
|
||||
</label>
|
||||
<label className="switch relative inline-flex items-center cursor-pointer h-[30px] w-[50px] shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={enabled}
|
||||
onChange={(e) => onToggle(e.target.checked)}
|
||||
/>
|
||||
<div className={`switch-slider w-full h-full rounded-[var(--radius-full)] bg-[color-mix(in_srgb,var(--text-color)_20%,transparent)] peer-checked:bg-[var(--accent-color)] transition-colors duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1) before:content-[''] before:absolute before:h-[26px] before:w-[26px] before:left-[2px] before:bottom-[2px] before:bg-white before:rounded-[var(--radius-full)] before:transition-transform before:duration-[0.4s] before:cubic-bezier(0.2,0.8,0.2,1) before:shadow-[0_1px_3px_rgba(0,0,0,0.2)] peer-checked:before:translate-x-[20px]`}></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<div className="space-y-4 pt-4 border-t border-[var(--glass-border)] animate-in fade-in slide-in-from-top-2">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-[var(--text-color)]">已授权密码</h4>
|
||||
|
||||
{passwords.length === 0 && (
|
||||
<p className="text-sm text-[var(--text-color-secondary)] italic">
|
||||
未设置密码。在至少添加一个密码之前,任何人都可以访问。
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{passwords.map((pwd, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-full)] text-sm transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<span className="font-mono">{showPassword ? pwd : '••••••'}</span>
|
||||
<button
|
||||
onClick={() => onRemove(pwd)}
|
||||
className="text-[var(--text-color-secondary)] hover:text-red-500 transition-colors"
|
||||
title="删除密码"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAdd} className="flex gap-2 items-start">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
placeholder="添加新密码..."
|
||||
className="w-full px-4 py-2 pr-10 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border border-[var(--glass-border)] focus:outline-none focus:border-[var(--accent-color)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent-color)_30%,transparent)] transition-all duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1) text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-color-secondary)] hover:text-[var(--text-color)] transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-xs text-red-500 pl-2">{error}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newPassword}
|
||||
className="p-2 bg-[var(--accent-color)] text-white rounded-[var(--radius-2xl)] hover:translate-y-[-2px] hover:brightness-110 shadow-[var(--shadow-sm)] hover:shadow-[0_4px_8px_var(--shadow-color)] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none transition-all duration-200"
|
||||
>
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
interface SettingsSectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
headerAction?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SettingsSection({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
headerAction,
|
||||
}: SettingsSectionProps) {
|
||||
return (
|
||||
<div className="bg-[var(--glass-bg)] backdrop-filter backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-6 mb-6 transition-all duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1) hover:translate-y-[-5px] hover:scale-[1.02] hover:shadow-[0_8px_24px_var(--shadow-color)] hover:z-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-color)]">{title}</h2>
|
||||
{headerAction && <div className="flex gap-2">{headerAction}</div>}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-sm text-[var(--text-color-secondary)] mb-6 leading-[1.6]">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,8 @@ export interface AppSettings {
|
||||
sortBy: SortOption;
|
||||
searchHistory: boolean;
|
||||
watchHistory: boolean;
|
||||
passwordAccess: boolean;
|
||||
accessPasswords: string[];
|
||||
}
|
||||
|
||||
import { exportSettings, importSettings, SEARCH_HISTORY_KEY, WATCH_HISTORY_KEY } from './settings-helpers';
|
||||
@@ -36,6 +38,8 @@ export const settingsStore = {
|
||||
sortBy: 'default',
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess: false,
|
||||
accessPasswords: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +50,8 @@ export const settingsStore = {
|
||||
sortBy: 'default',
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess: false,
|
||||
accessPasswords: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,6 +63,8 @@ export const settingsStore = {
|
||||
sortBy: parsed.sortBy || 'default',
|
||||
searchHistory: parsed.searchHistory !== undefined ? parsed.searchHistory : true,
|
||||
watchHistory: parsed.watchHistory !== undefined ? parsed.watchHistory : true,
|
||||
passwordAccess: parsed.passwordAccess !== undefined ? parsed.passwordAccess : false,
|
||||
accessPasswords: Array.isArray(parsed.accessPasswords) ? parsed.accessPasswords : [],
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
@@ -64,6 +72,8 @@ export const settingsStore = {
|
||||
sortBy: 'default',
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
passwordAccess: false,
|
||||
accessPasswords: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user