mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 19:27:33 +08:00
feat: add service log display in log page
This commit is contained in:
@@ -5,7 +5,7 @@ use std::process::Command;
|
||||
use tauri::State;
|
||||
|
||||
use crate::object::structs::AppState;
|
||||
use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir};
|
||||
use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir, get_service_log_path};
|
||||
|
||||
fn generate_random_password() -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -110,6 +110,7 @@ async fn execute_openlist_admin_set(
|
||||
|
||||
fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
|
||||
let logs_dir = get_app_logs_dir()?;
|
||||
let service_path = get_service_log_path()?;
|
||||
|
||||
let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
|
||||
PathBuf::from(dir)
|
||||
@@ -124,11 +125,13 @@ fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec
|
||||
Some("app") => paths.push(logs_dir.join("app.log")),
|
||||
Some("rclone") => paths.push(logs_dir.join("process_rclone.log")),
|
||||
Some("openlist_core") => paths.push(logs_dir.join("process_openlist_core.log")),
|
||||
None => {
|
||||
Some("service") => paths.push(service_path),
|
||||
Some("all") => {
|
||||
paths.push(openlist_log_base.join("log/log.log"));
|
||||
paths.push(logs_dir.join("app.log"));
|
||||
paths.push(logs_dir.join("process_rclone.log"));
|
||||
paths.push(logs_dir.join("process_openlist_core.log"));
|
||||
paths.push(service_path);
|
||||
}
|
||||
_ => return Err("Invalid log source".into()),
|
||||
}
|
||||
@@ -225,9 +228,13 @@ pub async fn get_logs(
|
||||
let mut logs = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
let content =
|
||||
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {path:?}: {e}"))?;
|
||||
logs.extend(content.lines().map(str::to_string));
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.map_err(|e| format!("Failed to read {path:?}: {e}"))?;
|
||||
logs.extend(content.lines().map(str::to_string));
|
||||
} else {
|
||||
log::info!("Log file does not exist: {path:?}");
|
||||
}
|
||||
}
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
@@ -106,3 +106,21 @@ pub fn get_default_openlist_data_dir() -> Result<PathBuf, String> {
|
||||
Ok(get_app_dir()?.join("data"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_service_log_path() -> Result<PathBuf, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
|
||||
let logs = PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("io.github.openlistteam.openlist.service.bundle")
|
||||
.join("openlist-desktop-service.log");
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Ok(get_app_dir()?.join("openlist-desktop-service.log"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ export class TauriAPI {
|
||||
|
||||
// --- Logs management ---
|
||||
static logs = {
|
||||
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<string[]> =>
|
||||
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<string[]> =>
|
||||
call('get_logs', { source: src }),
|
||||
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<boolean> =>
|
||||
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<boolean> =>
|
||||
call('clear_logs', { source: src }),
|
||||
adminPassword: (): Promise<string> => call('get_admin_password'),
|
||||
resetAdminPassword: (): Promise<string> => call('reset_admin_password'),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"reset": "Reset",
|
||||
"close": "Close",
|
||||
"minimize": "Minimize",
|
||||
@@ -102,7 +103,10 @@
|
||||
"subtitle": "Configure your OpenList Desktop application",
|
||||
"saveChanges": "Save Changes",
|
||||
"resetToDefaults": "Reset to defaults",
|
||||
"confirmReset": "Are you sure you want to reset all settings to defaults? This action cannot be undone.",
|
||||
"confirmReset": {
|
||||
"title": "Reset Settings",
|
||||
"message": "Are you sure you want to reset all settings to defaults? This action cannot be undone."
|
||||
},
|
||||
"saved": "Settings saved successfully!",
|
||||
"saveFailed": "Failed to save settings. Please try again.",
|
||||
"resetSuccess": "Settings reset to defaults successfully!",
|
||||
@@ -283,7 +287,8 @@
|
||||
"sources": {
|
||||
"all": "All Sources",
|
||||
"rclone": "Rclone",
|
||||
"openlist": "OpenList"
|
||||
"openlist": "OpenList",
|
||||
"service": "Service"
|
||||
},
|
||||
"actions": {
|
||||
"selectAll": "Select All (Ctrl+A)",
|
||||
@@ -303,7 +308,8 @@
|
||||
"stripAnsiColors": "Strip ANSI Colors"
|
||||
},
|
||||
"messages": {
|
||||
"confirmClear": "Are you sure you want to clear all logs?"
|
||||
"confirmClear": "Are you sure you want to clear all logs?",
|
||||
"confirmTitle": "Clear Logs"
|
||||
},
|
||||
"headers": {
|
||||
"timestamp": "Timestamp",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"reset": "重置",
|
||||
"close": "关闭",
|
||||
"minimize": "最小化",
|
||||
@@ -102,7 +103,10 @@
|
||||
"subtitle": "配置您的 OpenList 桌面应用程序",
|
||||
"saveChanges": "保存更改",
|
||||
"resetToDefaults": "重置为默认值",
|
||||
"confirmReset": "您确定要将所有设置重置为默认值吗?此操作无法撤消。",
|
||||
"confirmReset": {
|
||||
"title": "重置设置",
|
||||
"message": "您确定要将所有设置重置为默认值吗?此操作无法撤消。"
|
||||
},
|
||||
"saved": "设置保存成功!",
|
||||
"saveFailed": "保存设置失败,请重试。",
|
||||
"resetSuccess": "设置已重置为默认值!",
|
||||
@@ -283,7 +287,8 @@
|
||||
"sources": {
|
||||
"all": "所有来源",
|
||||
"rclone": "Rclone",
|
||||
"openlist": "OpenList"
|
||||
"openlist": "OpenList",
|
||||
"service": "服务"
|
||||
},
|
||||
"actions": {
|
||||
"selectAll": "全选 (Ctrl+A)",
|
||||
@@ -303,7 +308,8 @@
|
||||
"stripAnsiColors": "去除 ANSI 颜色"
|
||||
},
|
||||
"messages": {
|
||||
"confirmClear": "您确定要清除所有日志吗?"
|
||||
"confirmClear": "您确定要清除所有日志吗?",
|
||||
"confirmTitle": "清除日志"
|
||||
},
|
||||
"headers": {
|
||||
"timestamp": "时间",
|
||||
|
||||
@@ -553,7 +553,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs(source?: 'openlist' | 'rclone' | 'app') {
|
||||
async function loadLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
|
||||
try {
|
||||
source = source || 'openlist'
|
||||
const logEntries = await TauriAPI.logs.get(source)
|
||||
@@ -563,9 +563,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function clearLogs(source?: 'openlist' | 'rclone' | 'app') {
|
||||
async function clearLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
|
||||
try {
|
||||
loading.value = true
|
||||
source = source || 'openlist'
|
||||
const result = await TauriAPI.logs.clear(source)
|
||||
if (result) {
|
||||
logs.value = []
|
||||
|
||||
@@ -22,6 +22,9 @@ import {
|
||||
FolderOpen
|
||||
} from 'lucide-vue-next'
|
||||
import * as chrono from 'chrono-node'
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
|
||||
|
||||
type filterSourceType = 'openlist' | 'rclone' | 'app' | 'service' | 'all'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { t } = useTranslation()
|
||||
@@ -30,10 +33,10 @@ const searchInputRef = ref<HTMLInputElement>()
|
||||
const autoScroll = ref(true)
|
||||
const isPaused = ref(false)
|
||||
const filterLevel = ref<string>('all')
|
||||
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'all')
|
||||
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'openlist')
|
||||
const searchQuery = ref('')
|
||||
const selectedEntries = ref<Set<number>>(new Set())
|
||||
const showFilters = ref(false)
|
||||
const showFilters = ref(true)
|
||||
const showSettings = ref(false)
|
||||
const fontSize = ref(13)
|
||||
const maxLines = ref(1000)
|
||||
@@ -43,12 +46,17 @@ const stripAnsiColors = ref(true)
|
||||
const showNotification = ref(false)
|
||||
const notificationMessage = ref('')
|
||||
const notificationType = ref<'success' | 'info' | 'warning' | 'error'>('success')
|
||||
const showConfirmDialog = ref(false)
|
||||
const confirmDialogConfig = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {}
|
||||
})
|
||||
|
||||
watch(filterSource, async newValue => {
|
||||
localStorage.setItem('logFilterSource', newValue)
|
||||
await appStore.loadLogs(
|
||||
(newValue !== 'all' && newValue !== 'gin' ? newValue : 'openlist') as 'openlist' | 'rclone' | 'app'
|
||||
)
|
||||
await appStore.loadLogs((newValue !== 'gin' ? newValue : 'openlist') as filterSourceType)
|
||||
await scrollToBottom()
|
||||
})
|
||||
|
||||
@@ -115,11 +123,8 @@ const parseLogEntry = (logText: string) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cleanText.includes('openlist_desktop') || cleanText.includes('tao::')) {
|
||||
source = 'app'
|
||||
level = 'info'
|
||||
} else if (cleanText.toLowerCase().includes('rclone')) {
|
||||
source = 'rclone'
|
||||
} else {
|
||||
source = filterSource.value
|
||||
}
|
||||
|
||||
message = message
|
||||
@@ -198,21 +203,30 @@ const scrollToTop = () => {
|
||||
}
|
||||
|
||||
const clearLogs = async () => {
|
||||
if (confirm(t('logs.messages.confirmClear'))) {
|
||||
try {
|
||||
await appStore.clearLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
||||
| 'openlist'
|
||||
| 'rclone'
|
||||
| 'app'
|
||||
)
|
||||
selectedEntries.value.clear()
|
||||
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear logs:', error)
|
||||
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
|
||||
confirmDialogConfig.value = {
|
||||
title: t('logs.messages.confirmTitle') || t('common.confirm'),
|
||||
message: t('logs.messages.confirmClear'),
|
||||
onConfirm: async () => {
|
||||
showConfirmDialog.value = false
|
||||
try {
|
||||
await appStore.clearLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin'
|
||||
? filterSource.value
|
||||
: 'openlist') as filterSourceType
|
||||
)
|
||||
selectedEntries.value.clear()
|
||||
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear logs:', error)
|
||||
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
showConfirmDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
showConfirmDialog.value = true
|
||||
}
|
||||
|
||||
const copyLogsToClipboard = async () => {
|
||||
@@ -285,10 +299,7 @@ const togglePause = () => {
|
||||
|
||||
const refreshLogs = async () => {
|
||||
await appStore.loadLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
||||
| 'openlist'
|
||||
| 'rclone'
|
||||
| 'app'
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType
|
||||
)
|
||||
await scrollToBottom()
|
||||
if (isPaused.value) {
|
||||
@@ -363,28 +374,16 @@ const handleKeydown = (event: KeyboardEvent) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
appStore
|
||||
.loadLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
||||
| 'openlist'
|
||||
| 'rclone'
|
||||
| 'app'
|
||||
)
|
||||
.then(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType).then(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
|
||||
logRefreshInterval = setInterval(async () => {
|
||||
if (!isPaused.value) {
|
||||
const oldLength = appStore.logs.length
|
||||
await appStore.loadLogs(
|
||||
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
|
||||
| 'openlist'
|
||||
| 'rclone'
|
||||
| 'app'
|
||||
)
|
||||
await appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType)
|
||||
|
||||
if (appStore.logs.length > oldLength) {
|
||||
await scrollToBottom()
|
||||
@@ -532,6 +531,7 @@ onUnmounted(() => {
|
||||
<option value="openlist">{{ t('logs.filters.sources.openlist') }}</option>
|
||||
<option value="gin">GIN Server</option>
|
||||
<option value="rclone">{{ t('logs.filters.sources.rclone') }}</option>
|
||||
<option value="service">{{ t('logs.filters.sources.service') }}</option>
|
||||
<option value="app">{{ t('logs.filters.app') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -659,6 +659,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<ConfirmDialog
|
||||
:is-open="showConfirmDialog"
|
||||
:title="confirmDialogConfig.title"
|
||||
:message="confirmDialogConfig.message"
|
||||
:confirm-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
variant="danger"
|
||||
@confirm="confirmDialogConfig.onConfirm"
|
||||
@cancel="confirmDialogConfig.onCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
@@ -27,6 +28,13 @@ const activeTab = ref('openlist')
|
||||
const rcloneConfigJson = ref('')
|
||||
const autoStartApp = ref(false)
|
||||
const isResettingPassword = ref(false)
|
||||
const showConfirmDialog = ref(false)
|
||||
const confirmDialogConfig = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {}
|
||||
})
|
||||
|
||||
const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
|
||||
const rcloneSettings = reactive({ ...appStore.settings.rclone })
|
||||
@@ -167,24 +175,33 @@ const handleSave = async () => {
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
if (!confirm(t('settings.confirmReset'))) {
|
||||
return
|
||||
confirmDialogConfig.value = {
|
||||
title: t('settings.confirmReset.title'),
|
||||
message: t('settings.confirmReset.message'),
|
||||
onConfirm: async () => {
|
||||
showConfirmDialog.value = false
|
||||
|
||||
try {
|
||||
await appStore.resetSettings()
|
||||
Object.assign(openlistCoreSettings, appStore.settings.openlist)
|
||||
Object.assign(rcloneSettings, appStore.settings.rclone)
|
||||
Object.assign(appSettings, appStore.settings.app)
|
||||
|
||||
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
|
||||
|
||||
message.value = t('settings.resetSuccess')
|
||||
messageType.value = 'info'
|
||||
} catch (error) {
|
||||
message.value = t('settings.resetFailed')
|
||||
messageType.value = 'error'
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
showConfirmDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await appStore.resetSettings()
|
||||
Object.assign(openlistCoreSettings, appStore.settings.openlist)
|
||||
Object.assign(rcloneSettings, appStore.settings.rclone)
|
||||
Object.assign(appSettings, appStore.settings.app)
|
||||
|
||||
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
|
||||
|
||||
message.value = t('settings.resetSuccess')
|
||||
messageType.value = 'info'
|
||||
} catch (error) {
|
||||
message.value = t('settings.resetFailed')
|
||||
messageType.value = 'error'
|
||||
}
|
||||
showConfirmDialog.value = true
|
||||
}
|
||||
|
||||
const handleSelectDataDir = async () => {
|
||||
@@ -594,6 +611,17 @@ const loadCurrentAdminPassword = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
:is-open="showConfirmDialog"
|
||||
:title="confirmDialogConfig.title"
|
||||
:message="confirmDialogConfig.message"
|
||||
:confirm-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
variant="danger"
|
||||
@confirm="confirmDialogConfig.onConfirm"
|
||||
@cancel="confirmDialogConfig.onCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user