mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-17 22:03:50 +02:00
feat: add social status presets (#252)
This commit is contained in:
@@ -59,7 +59,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="presets.length" class="pb-4 px-16">
|
||||
<span class="text-xs text-muted-foreground mb-2 block">
|
||||
{{ t('dialog.social_status.presets') }}
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<div
|
||||
v-for="(preset, index) in presets"
|
||||
:key="index"
|
||||
class="group inline-flex items-center gap-1.5 pl-2.5 pr-1.5 py-1 rounded-full border bg-background text-xs cursor-pointer hover:bg-accent transition-colors max-w-[200px]"
|
||||
@click="applyPreset(preset)">
|
||||
<i class="x-user-status flex-none" :class="getStatusClass(preset.status)"></i>
|
||||
<span class="truncate">{{ preset.statusDescription || getStatusLabel(preset.status) }}</span>
|
||||
<button
|
||||
class="flex-none size-4 inline-flex items-center justify-center rounded-full opacity-0 group-hover:opacity-100 hover:bg-muted transition-opacity"
|
||||
@click.stop="handleDeletePreset(index)">
|
||||
<X class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="socialStatusDialog.loading" @click="handleSavePreset">
|
||||
<Bookmark class="size-4" />
|
||||
{{ t('dialog.social_status.save_preset') }}
|
||||
</Button>
|
||||
<Button :disabled="socialStatusDialog.loading" @click="saveSocialStatus">
|
||||
{{ t('dialog.social_status.update') }}
|
||||
</Button>
|
||||
@@ -77,7 +102,7 @@
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Check, History } from 'lucide-vue-next';
|
||||
import { Bookmark, Check, History, X } from 'lucide-vue-next';
|
||||
import { InputGroupButton, InputGroupField } from '@/components/ui/input-group';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { computed } from 'vue';
|
||||
@@ -87,6 +112,7 @@
|
||||
|
||||
import { useUserStore } from '../../../stores';
|
||||
import { userRequest } from '../../../api';
|
||||
import { useStatusPresets } from './composables/useStatusPresets';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
@@ -104,6 +130,35 @@
|
||||
|
||||
const historyItems = computed(() => props.socialStatusHistoryTable?.data ?? []);
|
||||
|
||||
const { presets, addPreset, removePreset, getStatusClass, MAX_PRESETS } = useStatusPresets();
|
||||
|
||||
function getStatusLabel(status) {
|
||||
const option = statusOptions.value.find((o) => o.value === status);
|
||||
return option?.label || status;
|
||||
}
|
||||
|
||||
function applyPreset(preset) {
|
||||
const D = props.socialStatusDialog;
|
||||
D.status = preset.status;
|
||||
D.statusDescription = preset.statusDescription;
|
||||
}
|
||||
|
||||
async function handleSavePreset() {
|
||||
const D = props.socialStatusDialog;
|
||||
const result = await addPreset(D.status, D.statusDescription);
|
||||
if (result === 'ok') {
|
||||
toast.success(t('dialog.social_status.preset_saved'));
|
||||
} else if (result === 'exists') {
|
||||
toast.info(t('dialog.social_status.preset_exists'));
|
||||
} else if (result === 'limit') {
|
||||
toast.warning(t('dialog.social_status.preset_limit', { max: MAX_PRESETS }));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeletePreset(index) {
|
||||
await removePreset(index);
|
||||
}
|
||||
|
||||
const statusOptions = computed(() => {
|
||||
const options = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ref } from 'vue';
|
||||
import configRepository from '../../../../services/config';
|
||||
|
||||
const STORAGE_KEY = 'VRCX_statusPresets';
|
||||
const MAX_PRESETS = 10;
|
||||
|
||||
const STATUS_CLASS_MAP = {
|
||||
'join me': 'joinme',
|
||||
active: 'online',
|
||||
'ask me': 'askme',
|
||||
busy: 'busy',
|
||||
offline: 'offline'
|
||||
};
|
||||
|
||||
const presets = ref([]);
|
||||
let loadPromise = null;
|
||||
|
||||
export function useStatusPresets() {
|
||||
async function loadPresets() {
|
||||
try {
|
||||
presets.value = await configRepository.getArray(STORAGE_KEY, []);
|
||||
} catch {
|
||||
presets.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function addPreset(status, statusDescription) {
|
||||
if (presets.value.length >= MAX_PRESETS) {
|
||||
return 'limit';
|
||||
}
|
||||
const exists = presets.value.some(
|
||||
(p) => p.status === status && p.statusDescription === statusDescription
|
||||
);
|
||||
if (exists) {
|
||||
return 'exists';
|
||||
}
|
||||
presets.value.push({ status, statusDescription });
|
||||
await configRepository.setArray(STORAGE_KEY, presets.value);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
async function removePreset(index) {
|
||||
presets.value.splice(index, 1);
|
||||
await configRepository.setArray(STORAGE_KEY, presets.value);
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
return STATUS_CLASS_MAP[status] || 'online';
|
||||
}
|
||||
|
||||
if (!loadPromise) {
|
||||
loadPromise = loadPresets();
|
||||
}
|
||||
|
||||
return { presets, loadPresets, addPreset, removePreset, getStatusClass, MAX_PRESETS };
|
||||
}
|
||||
@@ -1544,7 +1544,12 @@
|
||||
"header": "Social Status",
|
||||
"history": "History",
|
||||
"status_placeholder": "Status",
|
||||
"update": "Update"
|
||||
"update": "Update",
|
||||
"presets": "Presets",
|
||||
"save_preset": "Save as Preset",
|
||||
"preset_saved": "Preset saved",
|
||||
"preset_exists": "Preset already exists",
|
||||
"preset_limit": "Maximum {max} presets"
|
||||
},
|
||||
"language": {
|
||||
"header": "Language",
|
||||
|
||||
@@ -108,6 +108,21 @@
|
||||
</ContextMenuCheckboxItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSub v-if="statusPresets.length">
|
||||
<ContextMenuSubTrigger>
|
||||
{{ t('dialog.social_status.presets') }}
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
<ContextMenuItem
|
||||
v-for="(preset, idx) in statusPresets"
|
||||
:key="idx"
|
||||
class="gap-2"
|
||||
@click="applyStatusPreset(preset)">
|
||||
<i class="x-user-status" :class="presetStatusClass(preset.status)"></i>
|
||||
<span class="truncate max-w-[180px]">{{ getPresetDisplayText(preset) }}</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
@@ -218,6 +233,7 @@
|
||||
import FriendItem from './FriendItem.vue';
|
||||
import Location from '../../../components/Location.vue';
|
||||
import configRepository from '../../../services/config';
|
||||
import { useStatusPresets } from '../../../components/dialogs/UserDialog/composables/useStatusPresets';
|
||||
|
||||
import '@/styles/status-icon.css';
|
||||
import { showUserDialog } from '../../../coordinators/userCoordinator';
|
||||
@@ -252,6 +268,7 @@
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
const { checkCanInvite, checkCanInviteSelf } = useInviteChecks();
|
||||
const { userImage, userStatusClass } = useUserDisplay();
|
||||
const { presets: statusPresets, getStatusClass: presetStatusClass } = useStatusPresets();
|
||||
|
||||
const isFriendsGroupMe = ref(true);
|
||||
const isVIPFriends = ref(true);
|
||||
@@ -720,6 +737,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getPresetDisplayText(preset) {
|
||||
if (preset.statusDescription) return preset.statusDescription;
|
||||
const option = statusOptions.value.find((o) => o.value === preset.status);
|
||||
return option?.label || preset.status;
|
||||
}
|
||||
|
||||
function applyStatusPreset(preset) {
|
||||
userRequest
|
||||
.saveCurrentUser({
|
||||
status: preset.status,
|
||||
statusDescription: preset.statusDescription
|
||||
})
|
||||
.then(() => {
|
||||
toast.success('Status updated');
|
||||
});
|
||||
}
|
||||
|
||||
const canInviteToMyLocation = computed(() => checkCanInvite(lastLocation.value.location));
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,7 +60,9 @@ const mocks = vi.hoisted(() => ({
|
||||
},
|
||||
configRepository: {
|
||||
getBool: vi.fn(),
|
||||
setBool: vi.fn()
|
||||
setBool: vi.fn(),
|
||||
getArray: vi.fn(),
|
||||
setArray: vi.fn()
|
||||
},
|
||||
notificationRequest: {
|
||||
sendRequestInvite: vi.fn().mockResolvedValue({}),
|
||||
@@ -251,6 +253,8 @@ describe('FriendsSidebar.vue', () => {
|
||||
(_key, defaultValue) => Promise.resolve(defaultValue ?? false)
|
||||
);
|
||||
mocks.configRepository.setBool.mockResolvedValue(undefined);
|
||||
mocks.configRepository.getArray.mockResolvedValue([]);
|
||||
mocks.configRepository.setArray.mockResolvedValue(undefined);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user