mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
add avatar feed database cleanup settings and purge function
This commit is contained in:
@@ -1059,6 +1059,29 @@
|
||||
"video_play": "Video Play:",
|
||||
"event": "Event:"
|
||||
},
|
||||
"database_cleanup": {
|
||||
"header": "Database Cleanup",
|
||||
"auto_cleanup": "Auto-cleanup avatar data older than",
|
||||
"auto_cleanup_description": "Checked once per week on startup",
|
||||
"auto_cleanup_off": "Off",
|
||||
"auto_cleanup_30": "30 days",
|
||||
"auto_cleanup_90": "90 days",
|
||||
"auto_cleanup_180": "6 months",
|
||||
"auto_cleanup_365": "1 year",
|
||||
"purge_button": "Purge Avatar Feed Data",
|
||||
"purge": "Purge",
|
||||
"purge_older_than": "Delete avatar data older than",
|
||||
"purge_option_180": "6 months",
|
||||
"purge_option_365": "1 year",
|
||||
"purge_option_730": "2 years",
|
||||
"purge_option_all": "All data",
|
||||
"purge_confirm_title": "Purge Avatar Feed Data",
|
||||
"purge_confirm_description": "This will permanently delete avatar change records from the database and reclaim disk space. This action cannot be undone!\n\nIt is strongly recommended to back up your database file before proceeding.\n\nVRCX will restart after the operation completes.",
|
||||
"purge_confirm_button": "Purge & Restart",
|
||||
"purge_in_progress": "Purging avatar data...",
|
||||
"purge_complete": "Avatar data purged successfully. Restarting...",
|
||||
"purge_failed": "Purge failed: {error}"
|
||||
},
|
||||
"user_generated_content": {
|
||||
"header": "User Generated Content",
|
||||
"folder": "Open Folder",
|
||||
|
||||
@@ -67,6 +67,26 @@ const feed = {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Purges avatar feed data from the database.
|
||||
* !!!!
|
||||
* @param {string|null} cutoffDate - ISO date string. Deletes records older than this date. If null, deletes all records.
|
||||
*/
|
||||
async purgeAvatarFeedData(cutoffDate) {
|
||||
if (cutoffDate) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_feed_avatar WHERE created_at < @cutoff`,
|
||||
{
|
||||
'@cutoff': cutoffDate
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_feed_avatar`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
addOnlineOfflineToDatabase(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_feed_online_offline (created_at, user_id, display_name, type, location, world_name, time, group_name) VALUES (@created_at, @user_id, @display_name, @type, @location, @world_name, @time, @group_name)`,
|
||||
|
||||
@@ -839,6 +839,7 @@ export const useAuthStore = defineStore('Auth', () => {
|
||||
*/
|
||||
async function loginComplete() {
|
||||
await database.initUserTables(userStore.currentUser.id);
|
||||
advancedSettingsStore.runAvatarAutoCleanup(userStore.currentUser.id);
|
||||
watchState.isLoggedIn = true;
|
||||
AppApi.CheckGameRunning(); // restore state from hot-reload
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
const showConfirmationOnSwitchAvatar = ref(false);
|
||||
const gameLogDisabled = ref(false);
|
||||
const sqliteTableSizes = ref({});
|
||||
const avatarAutoCleanup = ref('Off');
|
||||
const purgeInProgress = ref(false);
|
||||
const ugcFolderPath = ref('');
|
||||
const autoDeleteOldPrints = ref(false);
|
||||
const notificationOpacity = ref(100);
|
||||
@@ -109,6 +111,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
progressPieFilterConfig,
|
||||
showConfirmationOnSwitchAvatarConfig,
|
||||
gameLogDisabledConfig,
|
||||
avatarAutoCleanupConfig,
|
||||
ugcFolderPathConfig,
|
||||
autoDeleteOldPrintsConfig,
|
||||
notificationOpacityConfig,
|
||||
@@ -157,6 +160,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
false
|
||||
),
|
||||
configRepository.getBool('VRCX_gameLogDisabled', false),
|
||||
configRepository.getString('VRCX_avatarAutoCleanup', 'Off'),
|
||||
configRepository.getString('VRCX_userGeneratedContentPath', ''),
|
||||
configRepository.getBool('VRCX_autoDeleteOldPrints', false),
|
||||
configRepository.getFloat('VRCX_notificationOpacity', 100),
|
||||
@@ -203,6 +207,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
showConfirmationOnSwitchAvatar.value =
|
||||
showConfirmationOnSwitchAvatarConfig;
|
||||
gameLogDisabled.value = gameLogDisabledConfig;
|
||||
avatarAutoCleanup.value = avatarAutoCleanupConfig;
|
||||
ugcFolderPath.value = ugcFolderPathConfig;
|
||||
autoDeleteOldPrints.value = autoDeleteOldPrintsConfig;
|
||||
notificationOpacity.value = notificationOpacityConfig;
|
||||
@@ -524,6 +529,101 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
);
|
||||
}
|
||||
|
||||
async function setAvatarAutoCleanup(value) {
|
||||
avatarAutoCleanup.value = value;
|
||||
await configRepository.setString('VRCX_avatarAutoCleanup', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number|null} days - Number of days to keep. Null means delete all.
|
||||
*/
|
||||
async function purgeAvatarFeedData(days) {
|
||||
let cutoffDate = null;
|
||||
if (days !== null) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
cutoffDate = cutoff.toJSON();
|
||||
}
|
||||
|
||||
purgeInProgress.value = true;
|
||||
const msgBox = toast.warning(
|
||||
t(
|
||||
'view.settings.advanced.advanced.database_cleanup.purge_in_progress'
|
||||
),
|
||||
{ duration: Infinity }
|
||||
);
|
||||
|
||||
try {
|
||||
await database.purgeAvatarFeedData(cutoffDate);
|
||||
await database.vacuum();
|
||||
toast.dismiss(msgBox);
|
||||
toast.success(
|
||||
t(
|
||||
'view.settings.advanced.advanced.database_cleanup.purge_complete'
|
||||
)
|
||||
);
|
||||
// Brief delay before restart to show success message
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 1500)
|
||||
);
|
||||
VRCXUpdaterStore.restartVRCX(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.dismiss(msgBox);
|
||||
toast.error(t('view.settings.advanced.advanced.database_cleanup.purge_failed', { error: err }));
|
||||
} finally {
|
||||
purgeInProgress.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run auto-cleanup on startup if configured and enough time has passed.
|
||||
* Reads config directly from configRepository to avoid race condition
|
||||
* with initAdvancedSettings not having completed yet.
|
||||
* @param {string} userId - Current user ID for per-user cleanup tracking.
|
||||
*/
|
||||
async function runAvatarAutoCleanup(userId) {
|
||||
const cleanupSetting = await configRepository.getString(
|
||||
'VRCX_avatarAutoCleanup',
|
||||
'Off'
|
||||
);
|
||||
if (cleanupSetting === 'Off') return;
|
||||
|
||||
const configKey = `VRCX_lastAvatarCleanupDate_${userId}`;
|
||||
const lastCleanupStr = await configRepository.getString(
|
||||
configKey,
|
||||
''
|
||||
);
|
||||
const now = new Date();
|
||||
|
||||
if (lastCleanupStr) {
|
||||
const lastCleanup = new Date(lastCleanupStr);
|
||||
const daysSinceLastCleanup =
|
||||
(now - lastCleanup) / (1000 * 60 * 60 * 24);
|
||||
if (daysSinceLastCleanup < 7) return;
|
||||
}
|
||||
|
||||
const days = parseInt(cleanupSetting, 10);
|
||||
if (isNaN(days) || days <= 0) return;
|
||||
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
const cutoffDate = cutoff.toJSON();
|
||||
|
||||
try {
|
||||
await database.purgeAvatarFeedData(cutoffDate);
|
||||
await configRepository.setString(
|
||||
configKey,
|
||||
now.toJSON()
|
||||
);
|
||||
console.log(
|
||||
`Auto-cleaned avatar feed data older than ${days} days`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Avatar auto-cleanup failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function setSaveInstanceEmoji() {
|
||||
saveInstanceEmoji.value = !saveInstanceEmoji.value;
|
||||
await configRepository.setBool(
|
||||
@@ -1030,6 +1130,8 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
showConfirmationOnSwitchAvatar,
|
||||
gameLogDisabled,
|
||||
sqliteTableSizes,
|
||||
avatarAutoCleanup,
|
||||
purgeInProgress,
|
||||
ugcFolderPath,
|
||||
currentUserInventory,
|
||||
autoDeleteOldPrints,
|
||||
@@ -1069,6 +1171,9 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
||||
setProgressPieFilter,
|
||||
setShowConfirmationOnSwitchAvatar,
|
||||
setGameLogDisabled,
|
||||
setAvatarAutoCleanup,
|
||||
purgeAvatarFeedData,
|
||||
runAvatarAutoCleanup,
|
||||
setUGCFolderPath,
|
||||
cropPrintsChanged,
|
||||
setAutoDeleteOldPrints,
|
||||
|
||||
@@ -247,7 +247,81 @@
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup :title="t('view.settings.advanced.advanced.database_cleanup.header')">
|
||||
<SettingsItem
|
||||
:label="t('view.settings.advanced.advanced.database_cleanup.auto_cleanup')"
|
||||
:description="t('view.settings.advanced.advanced.database_cleanup.auto_cleanup_description')">
|
||||
<Select :model-value="avatarAutoCleanup" @update:modelValue="setAvatarAutoCleanup">
|
||||
<SelectTrigger class="w-36">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Off">{{ t('view.settings.advanced.advanced.database_cleanup.auto_cleanup_off') }}</SelectItem>
|
||||
<SelectItem value="30">{{ t('view.settings.advanced.advanced.database_cleanup.auto_cleanup_30') }}</SelectItem>
|
||||
<SelectItem value="90">{{ t('view.settings.advanced.advanced.database_cleanup.auto_cleanup_90') }}</SelectItem>
|
||||
<SelectItem value="180">{{ t('view.settings.advanced.advanced.database_cleanup.auto_cleanup_180') }}</SelectItem>
|
||||
<SelectItem value="365">{{ t('view.settings.advanced.advanced.database_cleanup.auto_cleanup_365') }}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem :label="t('view.settings.advanced.advanced.database_cleanup.purge_button')">
|
||||
<Button size="sm" variant="outline" @click="isPurgeDialogVisible = true">
|
||||
<Trash2 class="h-4 w-4 mr-1" />
|
||||
{{ t('view.settings.advanced.advanced.database_cleanup.purge') }}
|
||||
</Button>
|
||||
</SettingsItem>
|
||||
</SettingsGroup>
|
||||
|
||||
<Dialog :open="isPurgeDialogVisible" @update:open="(open) => { if (!open) isPurgeDialogVisible = false; }">
|
||||
<DialogContent class="x-dialog sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('view.settings.advanced.advanced.database_cleanup.purge_confirm_title') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="warning" class="mb-3">
|
||||
<TriangleAlert />
|
||||
<AlertDescription>
|
||||
{{ t('view.settings.advanced.advanced.database_cleanup.purge_confirm_description') }}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<SettingsItem :label="t('view.settings.advanced.advanced.database_cleanup.purge_older_than')">
|
||||
<Select v-model="selectedPurgePeriod">
|
||||
<SelectTrigger class="w-36">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="180">{{ t('view.settings.advanced.advanced.database_cleanup.purge_option_180') }}</SelectItem>
|
||||
<SelectItem value="365">{{ t('view.settings.advanced.advanced.database_cleanup.purge_option_365') }}</SelectItem>
|
||||
<SelectItem value="730">{{ t('view.settings.advanced.advanced.database_cleanup.purge_option_730') }}</SelectItem>
|
||||
<SelectItem value="all">{{ t('view.settings.advanced.advanced.database_cleanup.purge_option_all') }}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsItem>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" size="sm" @click="isPurgeDialogVisible = false">
|
||||
{{ t('confirm.cancel_button') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
:disabled="purgeInProgress"
|
||||
@click="handlePurge">
|
||||
<Trash2 class="h-4 w-4 mr-1" />
|
||||
{{ t('view.settings.advanced.advanced.database_cleanup.purge_confirm_button') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<SettingsGroup :title="t('view.profile.config_json')">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<TooltipWrapper side="top" :content="t('view.profile.refresh_tooltip')">
|
||||
<Button class="rounded-full" size="icon-sm" variant="outline" @click="refreshConfigTreeData()">
|
||||
@@ -280,10 +354,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Languages, RefreshCcw, Trash2 } from 'lucide-vue-next';
|
||||
import { Languages, RefreshCcw, Trash2, TriangleAlert } from 'lucide-vue-next';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -368,6 +445,8 @@
|
||||
showConfirmationOnSwitchAvatar,
|
||||
gameLogDisabled,
|
||||
sqliteTableSizes,
|
||||
avatarAutoCleanup,
|
||||
purgeInProgress,
|
||||
sentryErrorReporting
|
||||
} = storeToRefs(advancedSettingsStore);
|
||||
|
||||
@@ -382,6 +461,8 @@
|
||||
setEnableAppLauncherRunProcessOnce,
|
||||
setShowConfirmationOnSwitchAvatar,
|
||||
getSqliteTableSizes,
|
||||
setAvatarAutoCleanup,
|
||||
purgeAvatarFeedData,
|
||||
showVRChatConfig,
|
||||
promptAutoClearVRCXCacheFrequency,
|
||||
setSentryErrorReporting
|
||||
@@ -394,6 +475,8 @@
|
||||
const isTranslationApiDialogVisible = ref(false);
|
||||
const configTreeData = ref({});
|
||||
const visits = ref(0);
|
||||
const selectedPurgePeriod = ref('180');
|
||||
const isPurgeDialogVisible = ref(false);
|
||||
|
||||
const cacheSize = reactive({
|
||||
cachedUsers: 0,
|
||||
@@ -406,6 +489,14 @@
|
||||
|
||||
const isLinux = computed(() => LINUX);
|
||||
|
||||
function handlePurge() {
|
||||
const days =
|
||||
selectedPurgePeriod.value === 'all'
|
||||
? null
|
||||
: parseInt(selectedPurgePeriod.value, 10);
|
||||
purgeAvatarFeedData(days);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user