mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-12 08:58:11 +00:00
refactor: 统一 Electron 子窗口与标签页窗口管理
将原有独立子窗口 (childWindow) 和标签页窗口 (webTabWindow) 合并为统一的 窗口管理系统,通过 mode 参数区分窗口类型: - mode='tab': 标签页模式(有导航栏,默认) - mode='window': 独立窗口模式(无导航栏) 主要变更: - 移除 createChildWindow、preCreateChildWindow 等独立窗口相关代码 - 扩展 createWebTabWindow 支持 mode 参数 - 简化前端 openWindow 调用,将 config 对象扁平化为顶层参数 - 更新所有调用点使用新的统一接口
This commit is contained in:
parent
731dbc5507
commit
790f5d4838
523
electron/electron.js
vendored
523
electron/electron.js
vendored
@ -72,14 +72,10 @@ let screenshotObj = null,
|
||||
// 窗口实例变量
|
||||
let mainWindow = null,
|
||||
mainTray = null,
|
||||
preloadWindow = null,
|
||||
mediaWindow = null;
|
||||
|
||||
// 独立子窗口管理
|
||||
let childWindow = [];
|
||||
|
||||
// 多窗口 Tab 管理
|
||||
// Map<windowId, {window, views: [{id, view, name, favicon}], activeTabId}>
|
||||
// Map<windowId, {window, views: [{id, view, name, favicon}], activeTabId, mode: 'tab'|'window'}>
|
||||
let webTabWindows = new Map();
|
||||
let webTabWindowIdCounter = 1;
|
||||
// 标签名称到标签位置的映射,用于复用已存在的标签
|
||||
@ -435,241 +431,6 @@ function createUpdaterWindow(updateTitle) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建预窗口
|
||||
*/
|
||||
function preCreateChildWindow() {
|
||||
if (preloadWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browser = new BrowserWindow({
|
||||
width: 360,
|
||||
height: 360,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
center: true,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: utils.getDefaultBackgroundColor(),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭事件
|
||||
browser.addListener('closed', () => {
|
||||
preloadWindow = null;
|
||||
})
|
||||
|
||||
// 设置 UA
|
||||
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
|
||||
browser.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
|
||||
|
||||
utils.loadUrl(browser, serverUrl, '/preload')
|
||||
|
||||
preloadWindow = browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建子窗口
|
||||
* @param args {name, path, hash, force, userAgent, config, webPreferences}
|
||||
* - config: {title, titleFixed, ...BrowserWindowConstructorOptions}
|
||||
*/
|
||||
function createChildWindow(args) {
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!utils.isJson(args)) {
|
||||
args = {path: args, config: {}}
|
||||
}
|
||||
args.path = args.path || args.url;
|
||||
|
||||
const name = args.name || "auto_" + utils.randomString(6);
|
||||
const wind = childWindow.find(item => item.name == name);
|
||||
let browser = wind ? wind.browser : null;
|
||||
let isPreload = false;
|
||||
// 清理已销毁但仍被引用的窗口,避免对失效对象调用方法
|
||||
if (browser && browser.isDestroyed && browser.isDestroyed()) {
|
||||
const index = childWindow.findIndex(item => item.name == name);
|
||||
if (index > -1) {
|
||||
childWindow.splice(index, 1);
|
||||
}
|
||||
browser = null;
|
||||
}
|
||||
if (browser) {
|
||||
browser.focus();
|
||||
if (args.force === false) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const config = args.config || {};
|
||||
const webPreferences = args.webPreferences || {};
|
||||
const options = Object.assign({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
center: true,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: utils.getDefaultBackgroundColor(),
|
||||
webPreferences: Object.assign({
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
}, webPreferences),
|
||||
}, config)
|
||||
|
||||
options.width = utils.normalizeSize(options.width, 1280)
|
||||
options.height = utils.normalizeSize(options.height, 800)
|
||||
options.minWidth = utils.normalizeSize(options.minWidth, 360)
|
||||
options.minHeight = utils.normalizeSize(options.minHeight, 360)
|
||||
if (!options.webPreferences.contextIsolation) {
|
||||
delete options.webPreferences.preload;
|
||||
}
|
||||
if (options.parent) {
|
||||
options.parent = mainWindow
|
||||
}
|
||||
|
||||
if (preloadWindow && !preloadWindow.isDestroyed?.() && Object.keys(webPreferences).length === 0) {
|
||||
// 使用预加载窗口
|
||||
browser = preloadWindow;
|
||||
preloadWindow = null;
|
||||
isPreload = true;
|
||||
options.title && browser.setTitle(options.title);
|
||||
options.parent && browser.setParentWindow(options.parent);
|
||||
browser.setSize(options.width, options.height);
|
||||
browser.setMinimumSize(options.minWidth, options.minHeight);
|
||||
browser.center();
|
||||
browser.setAutoHideMenuBar(options.autoHideMenuBar);
|
||||
browser.removeAllListeners("closed");
|
||||
setTimeout(() => onShowWindow(browser), 300)
|
||||
process.nextTick(() => setTimeout(() => onShowWindow(browser), 50));
|
||||
} else {
|
||||
// 创建新窗口
|
||||
browser = new BrowserWindow(options)
|
||||
loger.info("create new window")
|
||||
}
|
||||
|
||||
browser.on('page-title-updated', (event, title) => {
|
||||
if (title == "index.html" || options.titleFixed === true) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
browser.on('focus', () => {
|
||||
browser.webContents.send("browserWindowFocus", {})
|
||||
})
|
||||
|
||||
browser.on('blur', () => {
|
||||
browser.webContents.send("browserWindowBlur", {})
|
||||
})
|
||||
|
||||
browser.on('close', event => {
|
||||
if (!willQuitApp) {
|
||||
utils.onBeforeUnload(event, browser).then(() => {
|
||||
browser.hide()
|
||||
setTimeout(() => {
|
||||
browser.destroy()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
browser.on('closed', () => {
|
||||
const index = childWindow.findIndex(item => item.browser === browser);
|
||||
if (index > -1) {
|
||||
childWindow.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
browser.once('ready-to-show', () => {
|
||||
onShowWindow(browser);
|
||||
})
|
||||
|
||||
browser.webContents.once('dom-ready', () => {
|
||||
onShowWindow(browser);
|
||||
})
|
||||
|
||||
childWindow.push({ name, browser })
|
||||
}
|
||||
|
||||
// 设置 UA
|
||||
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
|
||||
browser.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0" + (args.userAgent ? (" " + args.userAgent) : ""));
|
||||
|
||||
// 新窗口处理
|
||||
browser.webContents.setWindowOpenHandler(({url}) => {
|
||||
if (allowedCalls.test(url)) {
|
||||
renderer.openExternal(url).catch(() => {})
|
||||
} else {
|
||||
utils.onBeforeOpenWindow(browser.webContents, url).then(() => {
|
||||
renderer.openExternal(url).catch(() => {})
|
||||
})
|
||||
}
|
||||
return {action: 'deny'}
|
||||
})
|
||||
|
||||
// 设置右键菜单
|
||||
electronMenu.webContentsMenu(browser.webContents)
|
||||
|
||||
// 设置导航快捷键(返回/前进)
|
||||
navigation.setup(browser)
|
||||
|
||||
// 加载地址
|
||||
const hash = `${args.hash || args.path}`;
|
||||
if (/^https?:/i.test(hash)) {
|
||||
// 完整 URL 直接加载
|
||||
browser.loadURL(hash).then(_ => { }).catch(_ => { })
|
||||
} else if (isPreload) {
|
||||
// preload 窗口尝试调用 __initializeApp,失败则 loadUrl
|
||||
browser
|
||||
.webContents
|
||||
.executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true)
|
||||
.catch(() => {
|
||||
utils.loadUrl(browser, serverUrl, hash)
|
||||
});
|
||||
} else {
|
||||
// 相对路径使用 loadUrl
|
||||
utils.loadUrl(browser, serverUrl, hash)
|
||||
}
|
||||
|
||||
// 预创建下一个窗口
|
||||
preCreateChildWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新子窗口
|
||||
* @param browser
|
||||
* @param args
|
||||
*/
|
||||
function updateChildWindow(browser, args) {
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!utils.isJson(args)) {
|
||||
args = {path: args, name: null}
|
||||
}
|
||||
|
||||
const hash = args.hash || args.path;
|
||||
if (hash) {
|
||||
utils.loadUrl(browser, serverUrl, hash)
|
||||
}
|
||||
if (args.name) {
|
||||
const er = childWindow.find(item => item.browser == browser);
|
||||
if (er) {
|
||||
er.name = args.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建媒体浏览器窗口
|
||||
* @param args
|
||||
@ -745,7 +506,10 @@ function createMediaWindow(args, type = 'image') {
|
||||
|
||||
/**
|
||||
* 创建内置浏览器窗口(支持多窗口)
|
||||
* @param args {url, windowId, position, afterId, insertIndex, name, force, userAgent, title, titleFixed, webPreferences, ...}
|
||||
* @param args {url, windowId, position, afterId, insertIndex, name, force, userAgent, title, titleFixed, webPreferences, mode, ...}
|
||||
* - mode: 'tab' | 'window'
|
||||
* - 'window': 独立窗口模式(无导航栏)
|
||||
* - 'tab': 标签页模式(默认,有导航栏)
|
||||
* @returns {number} 窗口ID
|
||||
*/
|
||||
function createWebTabWindow(args) {
|
||||
@ -757,7 +521,10 @@ function createWebTabWindow(args) {
|
||||
args = {url: args}
|
||||
}
|
||||
|
||||
// 如果有 name,先查找是否已存在同名标签
|
||||
const mode = args.mode || 'tab';
|
||||
const isWindowMode = mode === 'window';
|
||||
|
||||
// 如果有 name,先查找是否已存在同名标签/窗口
|
||||
if (args.name) {
|
||||
const existing = webTabNameMap.get(args.name);
|
||||
if (existing) {
|
||||
@ -765,7 +532,7 @@ function createWebTabWindow(args) {
|
||||
if (existingWindowData && existingWindowData.window && !existingWindowData.window.isDestroyed()) {
|
||||
const viewItem = existingWindowData.views.find(v => v.id === existing.tabId);
|
||||
if (viewItem && viewItem.view && !viewItem.view.webContents.isDestroyed()) {
|
||||
// 激活已存在的标签
|
||||
// 激活已存在的标签/窗口
|
||||
if (existingWindowData.window.isMinimized()) {
|
||||
existingWindowData.window.restore();
|
||||
}
|
||||
@ -790,12 +557,12 @@ function createWebTabWindow(args) {
|
||||
let windowData = windowId ? webTabWindows.get(windowId) : null;
|
||||
let webTabWindow = windowData ? windowData.window : null;
|
||||
|
||||
// 如果没有指定窗口或窗口不存在,查找第一个可用窗口或创建新窗口
|
||||
// 如果没有指定窗口或窗口不存在,查找可用窗口或创建新窗口
|
||||
if (!webTabWindow) {
|
||||
// 如果没有指定窗口,尝试使用第一个可用窗口
|
||||
if (!windowId) {
|
||||
// window 模式总是创建新窗口;tab 模式尝试使用第一个可用的 tab 窗口
|
||||
if (!isWindowMode && !windowId) {
|
||||
for (const [id, data] of webTabWindows) {
|
||||
if (data.window && !data.window.isDestroyed()) {
|
||||
if (data.window && !data.window.isDestroyed() && data.mode !== 'window') {
|
||||
windowId = id;
|
||||
windowData = data;
|
||||
webTabWindow = data.window;
|
||||
@ -807,11 +574,21 @@ function createWebTabWindow(args) {
|
||||
// 如果还是没有窗口,创建新窗口
|
||||
if (!webTabWindow) {
|
||||
windowId = webTabWindowIdCounter++;
|
||||
webTabWindow = createWebTabWindowInstance(windowId, args.position);
|
||||
// 从 args 中提取窗口尺寸
|
||||
const position = {
|
||||
x: args.x,
|
||||
y: args.y,
|
||||
width: args.width,
|
||||
height: args.height,
|
||||
minWidth: args.minWidth,
|
||||
minHeight: args.minHeight,
|
||||
};
|
||||
webTabWindow = createWebTabWindowInstance(windowId, position, mode);
|
||||
windowData = {
|
||||
window: webTabWindow,
|
||||
views: [],
|
||||
activeTabId: null
|
||||
activeTabId: null,
|
||||
mode: mode
|
||||
};
|
||||
webTabWindows.set(windowId, windowData);
|
||||
}
|
||||
@ -853,14 +630,17 @@ function createWebTabWindow(args) {
|
||||
});
|
||||
}
|
||||
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'create',
|
||||
id: browserView.webContents.id,
|
||||
url: args.url,
|
||||
afterId: args.afterId,
|
||||
windowId: windowId,
|
||||
title: args.title,
|
||||
}).then(_ => { });
|
||||
// tab 模式通知标签栏创建标签;window 模式不需要
|
||||
if (!isWindowMode) {
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'create',
|
||||
id: browserView.webContents.id,
|
||||
url: args.url,
|
||||
afterId: args.afterId,
|
||||
windowId: windowId,
|
||||
title: args.title,
|
||||
}).then(_ => { });
|
||||
}
|
||||
activateWebTabInWindow(windowId, browserView.webContents.id);
|
||||
|
||||
return windowId;
|
||||
@ -870,30 +650,40 @@ function createWebTabWindow(args) {
|
||||
* 创建 WebTabWindow 实例
|
||||
* @param windowId
|
||||
* @param position {x, y, width, height}
|
||||
* @param mode 'tab' | 'window'
|
||||
* @returns {BrowserWindow}
|
||||
*/
|
||||
function createWebTabWindowInstance(windowId, position) {
|
||||
const titleBarOverlay = {
|
||||
height: webTabHeight
|
||||
};
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
titleBarOverlay.color = '#3B3B3D';
|
||||
titleBarOverlay.symbolColor = '#C5C5C5';
|
||||
function createWebTabWindowInstance(windowId, position, mode = 'tab') {
|
||||
const isWindowMode = mode === 'window';
|
||||
|
||||
// mode='window': 根据屏幕分辨率动态计算默认值
|
||||
// mode='tab': 使用 userConf 保存值或固定默认值
|
||||
let defaultWidth, defaultHeight, defaultMinWidth, defaultMinHeight;
|
||||
if (isWindowMode) {
|
||||
const { width: screenWidth } = screen.getPrimaryDisplay().workAreaSize;
|
||||
const isHighRes = screenWidth >= 2560;
|
||||
defaultWidth = isHighRes ? 1920 : 1024;
|
||||
defaultHeight = isHighRes ? 1080 : 768;
|
||||
defaultMinWidth = 400;
|
||||
defaultMinHeight = 300;
|
||||
} else {
|
||||
const savedBounds = userConf.get('webTabWindow') || {};
|
||||
defaultWidth = savedBounds.width ?? 1280;
|
||||
defaultHeight = savedBounds.height ?? 800;
|
||||
defaultMinWidth = 360;
|
||||
defaultMinHeight = 360;
|
||||
}
|
||||
|
||||
const defaultBounds = userConf.get('webTabWindow') || {};
|
||||
const hasExplicitPosition = position?.x !== undefined && position?.y !== undefined;
|
||||
const windowOptions = {
|
||||
width: position?.width ?? defaultBounds.width ?? 1280,
|
||||
height: position?.height ?? defaultBounds.height ?? 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
width: position?.width ?? defaultWidth,
|
||||
height: position?.height ?? defaultHeight,
|
||||
minWidth: position?.minWidth ?? defaultMinWidth,
|
||||
minHeight: position?.minHeight ?? defaultMinHeight,
|
||||
center: !hasExplicitPosition,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
|
||||
backgroundColor: isWindowMode ? utils.getDefaultBackgroundColor() : (nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF'),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
@ -902,6 +692,19 @@ function createWebTabWindowInstance(windowId, position) {
|
||||
},
|
||||
};
|
||||
|
||||
// tab 模式使用隐藏标题栏 + titleBarOverlay
|
||||
if (!isWindowMode) {
|
||||
const titleBarOverlay = {
|
||||
height: webTabHeight
|
||||
};
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
titleBarOverlay.color = '#3B3B3D';
|
||||
titleBarOverlay.symbolColor = '#C5C5C5';
|
||||
}
|
||||
windowOptions.titleBarStyle = 'hidden';
|
||||
windowOptions.titleBarOverlay = titleBarOverlay;
|
||||
}
|
||||
|
||||
// 有明确位置时设置 x/y
|
||||
if (hasExplicitPosition) {
|
||||
windowOptions.x = position.x;
|
||||
@ -944,7 +747,11 @@ function createWebTabWindowInstance(windowId, position) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
userConf.set('webTabWindow', webTabWindow.getBounds());
|
||||
// 只有 tab 模式才保存 bounds
|
||||
const windowData = webTabWindows.get(windowId);
|
||||
if (windowData && windowData.mode !== 'window') {
|
||||
userConf.set('webTabWindow', webTabWindow.getBounds());
|
||||
}
|
||||
});
|
||||
|
||||
webTabWindow.on('closed', () => {
|
||||
@ -996,7 +803,10 @@ function createWebTabWindowInstance(windowId, position) {
|
||||
return item ? item.view.webContents : null;
|
||||
});
|
||||
|
||||
webTabWindow.loadFile('./render/tabs/index.html', {query: {windowId: String(windowId)}}).then(_ => { }).catch(_ => { });
|
||||
// tab 模式加载标签栏界面,window 模式不需要
|
||||
if (!isWindowMode) {
|
||||
webTabWindow.loadFile('./render/tabs/index.html', {query: {windowId: String(windowId)}}).then(_ => { }).catch(_ => { });
|
||||
}
|
||||
|
||||
return webTabWindow;
|
||||
}
|
||||
@ -1012,12 +822,16 @@ function createWebTabView(windowId, args) {
|
||||
if (!windowData) return null;
|
||||
|
||||
const webTabWindow = windowData.window;
|
||||
const viewOptions = args.config || {};
|
||||
viewOptions.webPreferences = Object.assign({
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true
|
||||
}, args.webPreferences || {});
|
||||
const isWindowMode = windowData.mode === 'window';
|
||||
const effectiveTabHeight = isWindowMode ? 0 : webTabHeight;
|
||||
|
||||
const viewOptions = {
|
||||
webPreferences: Object.assign({
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true
|
||||
}, args.webPreferences || {})
|
||||
};
|
||||
if (!viewOptions.webPreferences.contextIsolation) {
|
||||
delete viewOptions.webPreferences.preload;
|
||||
}
|
||||
@ -1025,6 +839,8 @@ function createWebTabView(windowId, args) {
|
||||
const browserView = new WebContentsView(viewOptions);
|
||||
if (args.backgroundColor) {
|
||||
browserView.setBackgroundColor(args.backgroundColor);
|
||||
} else if (isWindowMode) {
|
||||
browserView.setBackgroundColor(utils.getDefaultBackgroundColor());
|
||||
} else if (nativeTheme.shouldUseDarkColors) {
|
||||
browserView.setBackgroundColor('#575757');
|
||||
} else {
|
||||
@ -1032,9 +848,9 @@ function createWebTabView(windowId, args) {
|
||||
}
|
||||
browserView.setBounds({
|
||||
x: 0,
|
||||
y: webTabHeight,
|
||||
y: effectiveTabHeight,
|
||||
width: webTabWindow.getContentBounds().width || 1280,
|
||||
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
|
||||
height: (webTabWindow.getContentBounds().height || 800) - effectiveTabHeight,
|
||||
});
|
||||
|
||||
// 保存所属窗口ID和元数据
|
||||
@ -1063,7 +879,13 @@ function createWebTabView(windowId, args) {
|
||||
browserView.webContents.setWindowOpenHandler(({url}) => {
|
||||
if (allowedCalls.test(url)) {
|
||||
renderer.openExternal(url).catch(() => {});
|
||||
} else if (isWindowMode) {
|
||||
// window 模式下打开外部浏览器
|
||||
utils.onBeforeOpenWindow(browserView.webContents, url).then(() => {
|
||||
renderer.openExternal(url).catch(() => {});
|
||||
});
|
||||
} else {
|
||||
// tab 模式下创建新标签
|
||||
createWebTabWindow({url, afterId: browserView.webContents.id, windowId});
|
||||
}
|
||||
return {action: 'deny'};
|
||||
@ -1323,15 +1145,18 @@ function resizeWebTabInWindow(windowId, id) {
|
||||
if (!windowData || !windowData.window) return;
|
||||
|
||||
const webTabWindow = windowData.window;
|
||||
const isWindowMode = windowData.mode === 'window';
|
||||
const effectiveTabHeight = isWindowMode ? 0 : webTabHeight;
|
||||
|
||||
const item = id === 0 ? currentWebTabInWindow(windowId) : windowData.views.find(item => item.id == id);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
item.view.setBounds({
|
||||
x: 0,
|
||||
y: webTabHeight,
|
||||
y: effectiveTabHeight,
|
||||
width: webTabWindow.getContentBounds().width || 1280,
|
||||
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
|
||||
height: (webTabWindow.getContentBounds().height || 800) - effectiveTabHeight,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1373,10 +1198,22 @@ function closeWebTabInWindow(windowId, id) {
|
||||
|
||||
const webTabView = windowData.views;
|
||||
const webTabWindow = windowData.window;
|
||||
const isWindowMode = windowData.mode === 'window';
|
||||
|
||||
const item = id === 0 ? currentWebTabInWindow(windowId) : webTabView.find(item => item.id == id);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
// window 模式下直接关闭整个窗口
|
||||
if (isWindowMode) {
|
||||
webTabView.forEach(({name}) => {
|
||||
if (name) webTabNameMap.delete(name);
|
||||
});
|
||||
webTabWindow.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (webTabView.length === 1) {
|
||||
webTabWindow.hide();
|
||||
}
|
||||
@ -1668,9 +1505,7 @@ function monitorThemeChanges() {
|
||||
// 更新背景
|
||||
const backgroundColor = utils.getDefaultBackgroundColor()
|
||||
mainWindow?.setBackgroundColor(backgroundColor);
|
||||
preloadWindow?.setBackgroundColor(backgroundColor);
|
||||
mediaWindow?.setBackgroundColor(backgroundColor);
|
||||
childWindow.some(({browser}) => browser.setBackgroundColor(backgroundColor))
|
||||
// 更新所有 webTab 窗口背景
|
||||
for (const [, windowData] of webTabWindows) {
|
||||
windowData.window?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF');
|
||||
@ -1706,8 +1541,6 @@ if (!getTheLock) {
|
||||
utils.useCookie()
|
||||
// 创建主窗口
|
||||
createMainWindow()
|
||||
// 预创建子窗口
|
||||
preCreateChildWindow()
|
||||
// 监听主题变化
|
||||
monitorThemeChanges()
|
||||
// 创建托盘
|
||||
@ -1803,42 +1636,39 @@ ipcMain.on('windowQuit', (event) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 显示预加载窗口(用于调试)
|
||||
*/
|
||||
ipcMain.on('showPreloadWindow', (event) => {
|
||||
if (preloadWindow) {
|
||||
onShowWindow(preloadWindow)
|
||||
}
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新路由窗口
|
||||
* @param args {?name, ?path} // name: 不是要更改的窗口名,是要把窗口名改成什么, path: 地址
|
||||
*/
|
||||
ipcMain.on('updateChildWindow', (event, args) => {
|
||||
const browser = BrowserWindow.fromWebContents(event.sender);
|
||||
updateChildWindow(browser, args)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取路由窗口信息
|
||||
* 获取路由窗口信息(从 webTabWindows 中查找 mode='window' 的窗口)
|
||||
*/
|
||||
ipcMain.handle('getChildWindow', (event, args) => {
|
||||
let child;
|
||||
let windowData, viewItem;
|
||||
if (!args) {
|
||||
const browser = BrowserWindow.fromWebContents(event.sender);
|
||||
child = childWindow.find(({browser: win}) => win === browser)
|
||||
} else {
|
||||
child = childWindow.find(({name}) => name === args)
|
||||
}
|
||||
if (child) {
|
||||
return {
|
||||
name: child.name,
|
||||
id: child.browser.webContents.id,
|
||||
url: child.browser.webContents.getURL()
|
||||
// 通过发送者查找
|
||||
const sender = event.sender;
|
||||
for (const [, data] of webTabWindows) {
|
||||
if (data.mode === 'window') {
|
||||
const found = data.views.find(v => v.view.webContents === sender);
|
||||
if (found) {
|
||||
windowData = data;
|
||||
viewItem = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 通过名称查找
|
||||
const location = webTabNameMap.get(args);
|
||||
if (location) {
|
||||
windowData = webTabWindows.get(location.windowId);
|
||||
if (windowData && windowData.mode === 'window') {
|
||||
viewItem = windowData.views.find(v => v.id === location.tabId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (windowData && viewItem) {
|
||||
return {
|
||||
name: viewItem.name,
|
||||
id: viewItem.view.webContents.id,
|
||||
url: viewItem.view.webContents.getURL()
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@ -1857,17 +1687,12 @@ ipcMain.on('openMediaViewer', (event, args) => {
|
||||
* - url: 要打开的地址
|
||||
* - name: 窗口/标签名称
|
||||
* - mode: 'tab' | 'window'
|
||||
* - 'window': 独立窗口模式
|
||||
* - 'tab': 标签页模式(默认)
|
||||
* - 'window': 独立窗口模式(无导航栏)
|
||||
* - 'tab': 标签页模式(默认,有导航栏)
|
||||
*/
|
||||
ipcMain.on('openWindow', (event, args) => {
|
||||
if (args.mode === 'window') {
|
||||
// 独立窗口模式
|
||||
createChildWindow(args)
|
||||
} else {
|
||||
// 标签页模式
|
||||
createWebTabWindow(args)
|
||||
}
|
||||
// 统一使用 createWebTabWindow,通过 mode 区分窗口类型
|
||||
createWebTabWindow(args)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
@ -2263,40 +2088,33 @@ ipcMain.on('windowDestroy', (event) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 关闭所有子窗口
|
||||
* 关闭所有子窗口(mode='window' 的窗口)
|
||||
*/
|
||||
ipcMain.on('childWindowCloseAll', (event) => {
|
||||
childWindow.some(({browser}) => {
|
||||
browser && browser.close()
|
||||
})
|
||||
preloadWindow?.close()
|
||||
for (const [, data] of webTabWindows) {
|
||||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||||
data.window.close();
|
||||
}
|
||||
}
|
||||
mediaWindow?.close()
|
||||
electronDown.close()
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 销毁所有子窗口
|
||||
* 销毁所有子窗口(mode='window' 的窗口)
|
||||
*/
|
||||
ipcMain.on('childWindowDestroyAll', (event) => {
|
||||
childWindow.some(({browser}) => {
|
||||
browser && browser.destroy()
|
||||
})
|
||||
preloadWindow?.destroy()
|
||||
for (const [, data] of webTabWindows) {
|
||||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||||
data.window.destroy();
|
||||
}
|
||||
}
|
||||
mediaWindow?.destroy()
|
||||
electronDown.destroy()
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 刷新预加载窗口(用于更换语言和主题时触发)
|
||||
*/
|
||||
ipcMain.on('reloadPreloadWindow', (event) => {
|
||||
if (preloadWindow) {
|
||||
preloadWindow.webContents.reload()
|
||||
}
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 设置窗口尺寸
|
||||
@ -2637,10 +2455,11 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
|
||||
|
||||
// 关闭所有子窗口
|
||||
willQuitApp = true
|
||||
childWindow.some(({browser}) => {
|
||||
browser && browser.destroy()
|
||||
})
|
||||
preloadWindow?.destroy()
|
||||
for (const [, data] of webTabWindows) {
|
||||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||||
data.window.destroy();
|
||||
}
|
||||
}
|
||||
mediaWindow?.destroy()
|
||||
electronDown.destroy()
|
||||
|
||||
|
||||
@ -276,6 +276,19 @@ export default {
|
||||
params.path = params.url
|
||||
delete params.url
|
||||
}
|
||||
// 兼容旧格式:将 config 扁平化
|
||||
if ($A.isJson(params.config)) {
|
||||
const config = params.config
|
||||
delete params.config
|
||||
params = Object.assign({
|
||||
title: config.title,
|
||||
titleFixed: config.titleFixed,
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
minWidth: config.minWidth,
|
||||
minHeight: config.minHeight,
|
||||
}, params)
|
||||
}
|
||||
this.$store.dispatch('openWindow', params);
|
||||
},
|
||||
openTabWindow: (url) => {
|
||||
@ -482,16 +495,17 @@ export default {
|
||||
await $A.IDBSet("cacheMicroApps", $A.cloneJSON(apps));
|
||||
|
||||
if (this.$Electron) {
|
||||
const mergedConfig = Object.assign({
|
||||
title: appConfig.title || ' ',
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
}, $A.isJson(windowConfig) ? windowConfig : {});
|
||||
await this.$store.dispatch('openWindow', {
|
||||
name: `single-apps-${$A.randomString(6)}`,
|
||||
path: path,
|
||||
force: false,
|
||||
config: Object.assign({
|
||||
title: appConfig.title || ' ',
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
}, $A.isJson(windowConfig) ? windowConfig : {}),
|
||||
title: mergedConfig.title,
|
||||
width: mergedConfig.width,
|
||||
height: mergedConfig.height,
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
await this.$store.dispatch('openAppChildPage', {
|
||||
@ -517,13 +531,9 @@ export default {
|
||||
await this.$store.dispatch('openWindow', {
|
||||
name: `external-apps-${$A.randomString(6)}`,
|
||||
path: config.url,
|
||||
force: false,
|
||||
config: {
|
||||
title: config.title || ' ',
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
},
|
||||
title: config.title || ' ',
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
await this.$store.dispatch('openAppChildPage', {
|
||||
@ -783,7 +793,7 @@ export default {
|
||||
|
||||
/**
|
||||
* 解析类型
|
||||
* @param type
|
||||
* @param type
|
||||
*/
|
||||
resolveType(type) {
|
||||
if (typeof type === 'string') {
|
||||
|
||||
2
resources/assets/js/language/index.js
vendored
2
resources/assets/js/language/index.js
vendored
@ -95,7 +95,7 @@ function setLanguage(language, silence = false) {
|
||||
if (silence) {
|
||||
utils.saveLanguage(language);
|
||||
(async () => {
|
||||
await $A.IDBDel("callAt")
|
||||
$A.IDBDel("callAt")
|
||||
$A.Electron?.sendMessage('reloadPreloadWindow');
|
||||
$A.reloadUrl()
|
||||
})()
|
||||
|
||||
@ -3784,15 +3784,11 @@ export default {
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `file-msg-${data.id}`,
|
||||
path: path,
|
||||
title,
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
userAgent: "/hideenOfficeTitle/",
|
||||
force: false,
|
||||
config: {
|
||||
title,
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
},
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
this.$store.dispatch('openAppChildPage', {
|
||||
|
||||
@ -184,15 +184,11 @@ export default {
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `file-${this.fileId}-${row.id}`,
|
||||
path: path,
|
||||
title,
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
userAgent: "/hideenOfficeTitle/",
|
||||
force: false,
|
||||
config: {
|
||||
title,
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
},
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
this.$store.dispatch('openAppChildPage', {
|
||||
|
||||
@ -394,13 +394,6 @@ export default {
|
||||
},
|
||||
}).then(linkRes => {
|
||||
// 使用子窗口打开会议
|
||||
const config = {
|
||||
title: this.addData.name,
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
}
|
||||
const meetingPath = $A.urlAddParams($A.removeMainUrlPrefix(linkRes.data), {
|
||||
type: 'direct',
|
||||
nickname: data.nickname,
|
||||
@ -412,9 +405,10 @@ export default {
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `meeting-window`,
|
||||
path: meetingPath,
|
||||
mode: 'window',
|
||||
force: false,
|
||||
config
|
||||
title: this.addData.name,
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
});
|
||||
// 关闭弹窗
|
||||
this.addShow = false;
|
||||
|
||||
@ -222,13 +222,9 @@ export default {
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `project-log-${id}`,
|
||||
path: path,
|
||||
force: false,
|
||||
config: {
|
||||
title: this.$L(title),
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
},
|
||||
title: this.$L(title),
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
e.preventDefault()
|
||||
|
||||
@ -104,18 +104,13 @@ export default {
|
||||
this.detailData = row;
|
||||
this.$emit("on-read");
|
||||
if (this.$Electron) {
|
||||
let config = {
|
||||
title: row.title,
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
}
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `report-detail-${row.id}`,
|
||||
path: `/single/report/detail/${row.id}`,
|
||||
force: false,
|
||||
config
|
||||
title: row.title,
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
});
|
||||
} else {
|
||||
this.showDetailDrawer = true;
|
||||
@ -128,17 +123,12 @@ export default {
|
||||
|
||||
onEditReport(id) {
|
||||
if (this.$Electron) {
|
||||
let config = {
|
||||
title: this.$L(id > 0 ? '修改报告' : '新增报告'),
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
}
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `report-edit-${id}`,
|
||||
path: `/single/report/edit/${id}`,
|
||||
force: false,
|
||||
config
|
||||
title: this.$L(id > 0 ? '修改报告' : '新增报告'),
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
});
|
||||
} else {
|
||||
this.reportId = id;
|
||||
|
||||
@ -172,14 +172,10 @@ export default {
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `task-content-${this.taskId}-${row.id}`,
|
||||
path: path,
|
||||
force: false,
|
||||
config: {
|
||||
title: title,
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
},
|
||||
title: title,
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
this.$store.dispatch('openAppChildPage', {
|
||||
|
||||
@ -1891,26 +1891,16 @@ export default {
|
||||
},
|
||||
|
||||
openNewWin() {
|
||||
const config = {
|
||||
title: this.taskDetail.name,
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth * 0.8, this.$el.clientWidth + 72),
|
||||
height: Math.min(window.screen.availHeight * 0.8, this.$el.clientHeight + 72),
|
||||
minWidth: 600,
|
||||
minHeight: 450,
|
||||
autoZoom: true,
|
||||
};
|
||||
if (this.hasOpenDialog) {
|
||||
config.minWidth = 800;
|
||||
config.minHeight = 600;
|
||||
}
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `task-${this.taskDetail.id}`,
|
||||
path: `/single/task/${this.taskDetail.id}?navActive=${this.navActive}`,
|
||||
mode: 'window',
|
||||
force: false,
|
||||
config
|
||||
title: this.taskDetail.name,
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth * 0.8, this.$el.clientWidth + 72),
|
||||
height: Math.min(window.screen.availHeight * 0.8, this.$el.clientHeight + 72),
|
||||
minWidth: this.hasOpenDialog ? 800 : 600,
|
||||
minHeight: this.hasOpenDialog ? 600 : 450,
|
||||
});
|
||||
this.$store.dispatch('openTask', 0);
|
||||
},
|
||||
@ -1971,15 +1961,11 @@ export default {
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `file-task-${file.id}`,
|
||||
path: path,
|
||||
title: `${file.name} (${$A.bytesToSize(file.size)})`,
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
userAgent: "/hideenOfficeTitle/",
|
||||
force: false,
|
||||
config: {
|
||||
title: `${file.name} (${$A.bytesToSize(file.size)})`,
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
},
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
this.$store.dispatch('openAppChildPage', {
|
||||
|
||||
@ -1523,15 +1523,11 @@ export default {
|
||||
this.$store.dispatch('openWindow', {
|
||||
name: `file-${item.id}`,
|
||||
path: path,
|
||||
title: $A.getFileName(item),
|
||||
titleFixed: true,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
userAgent: "/hideenOfficeTitle/",
|
||||
force: false, // 如果窗口已存在不重新加载
|
||||
config: {
|
||||
title: $A.getFileName(item),
|
||||
titleFixed: true,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1440),
|
||||
height: Math.min(window.screen.availHeight, 900),
|
||||
},
|
||||
});
|
||||
} else if (this.$isEEUIApp) {
|
||||
this.$store.dispatch('openAppChildPage', {
|
||||
|
||||
30
resources/assets/js/store/actions.js
vendored
30
resources/assets/js/store/actions.js
vendored
@ -1385,12 +1385,15 @@ export default {
|
||||
/**
|
||||
* 打开窗口(客户端)
|
||||
* @param dispatch
|
||||
* @param params {path, name, mode, force, config, userAgent, webPreferences}
|
||||
* @param params {path, name, mode, force, title, titleFixed, width, height, minWidth, minHeight, userAgent, webPreferences}
|
||||
* - path: 要打开的地址(或直接传 URL 字符串)
|
||||
* - name: 窗口/标签名称
|
||||
* - mode: 'tab' | 'window',默认 'tab'
|
||||
* - force: 是否强制刷新
|
||||
* - config: 窗口配置(独立窗口模式有效)
|
||||
* - title: 窗口标题
|
||||
* - titleFixed: 是否固定标题
|
||||
* - width/height: 窗口尺寸(mode='window' 有效)
|
||||
* - minWidth/minHeight: 最小尺寸(mode='window' 有效)
|
||||
* - userAgent: 自定义 UserAgent
|
||||
* - webPreferences: 网页偏好设置
|
||||
*/
|
||||
@ -1410,14 +1413,17 @@ export default {
|
||||
}
|
||||
|
||||
$A.Electron.sendMessage('openWindow', {
|
||||
url: params.path,
|
||||
name: params.name,
|
||||
url: params.path,
|
||||
mode: params.mode,
|
||||
force: params.force,
|
||||
config: params.config,
|
||||
title: params.title,
|
||||
titleFixed: params.titleFixed,
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
minWidth: params.minWidth,
|
||||
minHeight: params.minHeight,
|
||||
userAgent: params.userAgent,
|
||||
title: params.config?.title,
|
||||
titleFixed: params.config?.titleFixed,
|
||||
force: params.force,
|
||||
webPreferences: params.webPreferences,
|
||||
})
|
||||
},
|
||||
@ -3594,13 +3600,9 @@ export default {
|
||||
name: `dialog-${dialogId}`,
|
||||
path: `/single/dialog/${dialogId}`,
|
||||
mode: 'window',
|
||||
force: false,
|
||||
config: {
|
||||
title: dialogData.name,
|
||||
parent: null,
|
||||
width: Math.min(window.screen.availWidth, 1024),
|
||||
height: Math.min(window.screen.availHeight, 768),
|
||||
},
|
||||
title: dialogData.name,
|
||||
width: Math.min(window.screen.availWidth, 1024),
|
||||
height: Math.min(window.screen.availHeight, 768),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
5
resources/assets/js/utils/file.js
vendored
5
resources/assets/js/utils/file.js
vendored
@ -68,9 +68,12 @@ export function openFileInClient(vm, item, options = {}) {
|
||||
vm.$store.dispatch('openWindow', {
|
||||
name: windowName,
|
||||
path,
|
||||
title: windowConfig.title,
|
||||
titleFixed: windowConfig.titleFixed,
|
||||
width: windowConfig.width,
|
||||
height: windowConfig.height,
|
||||
userAgent: "/hideenOfficeTitle/",
|
||||
force: options.force === undefined ? false : options.force,
|
||||
config: windowConfig,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user