20241030191200
This commit is contained in:
parent
ae38969f30
commit
fd1605c246
179
main.py
179
main.py
|
@ -1,16 +1,19 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import io
|
||||||
import sys
|
import sys
|
||||||
import ctypes
|
import ctypes
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
import subprocess
|
import subprocess
|
||||||
import win32com.client
|
import win32com.client
|
||||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QDialog, QLabel, QComboBox, QCheckBox, QLineEdit, QAction, QMenu, QMessageBox, QPushButton, QTableWidget, QVBoxLayout, QHBoxLayout, QTableWidgetItem
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QDialog, QLabel, QComboBox, QCheckBox, QLineEdit, QAction, QMenu, QMessageBox, QPushButton, QTableWidget, QVBoxLayout, QHBoxLayout, QTableWidgetItem
|
||||||
from PyQt5.QtCore import Qt, QCoreApplication, QTimer
|
from PyQt5.QtCore import Qt, QCoreApplication, QPropertyAnimation, QTimer
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
|
|
||||||
|
|
||||||
|
@ -257,7 +260,7 @@ class BtSdk:
|
||||||
return self.dll.Btsdk_IsBluetoothReady()
|
return self.dll.Btsdk_IsBluetoothReady()
|
||||||
|
|
||||||
def enableDeviceDiscovery(self):
|
def enableDeviceDiscovery(self):
|
||||||
device_class, max_dev_num, max_durations = 0X000400, 30, 7
|
device_class, max_dev_num, max_durations = 0X000400, 30, 10
|
||||||
code = self.dll.Btsdk_StartDeviceDiscovery(device_class, max_dev_num, max_durations)
|
code = self.dll.Btsdk_StartDeviceDiscovery(device_class, max_dev_num, max_durations)
|
||||||
code != 0 and warnings.warn(self.errs[code])
|
code != 0 and warnings.warn(self.errs[code])
|
||||||
return not code
|
return not code
|
||||||
|
@ -376,6 +379,18 @@ class BtSdk:
|
||||||
self.dll.Btsdk_DeleteUnpairedDevicesByClass(0)
|
self.dll.Btsdk_DeleteUnpairedDevicesByClass(0)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def isBluetoothActive(self):
|
||||||
|
data = []
|
||||||
|
enum_handle = self.dll.Btsdk_StartEnumConnection()
|
||||||
|
while True:
|
||||||
|
conn = self.dll.Btsdk_EnumConnection(enum_handle, 0)
|
||||||
|
if (not conn) == 1:
|
||||||
|
self.dll.Btsdk_EndEnumConnection(enum_handle)
|
||||||
|
break
|
||||||
|
data.append(conn)
|
||||||
|
return True if len(data) > 0 else False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _fd(f):
|
def _fd(f):
|
||||||
return f.fileno() if hasattr(f, 'fileno') else f
|
return f.fileno() if hasattr(f, 'fileno') else f
|
||||||
|
@ -473,6 +488,23 @@ else:
|
||||||
return fcntl.flock(_fd(f), flags) == 0
|
return fcntl.flock(_fd(f), flags) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def getJson(file, data=None):
|
||||||
|
if os.path.exists(file):
|
||||||
|
try:
|
||||||
|
return json.loads(open(file=file, mode='r', encoding='utf-8').read())
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
return data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def putJson(file, data=None):
|
||||||
|
with open(file=file, mode='w', encoding='utf-8') as f:
|
||||||
|
flock(f, LOCK_EX)
|
||||||
|
res = f.write(json.dumps(data, indent=4, ensure_ascii=True))
|
||||||
|
flock(f, LOCK_UN)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class LoggerFileHandler:
|
class LoggerFileHandler:
|
||||||
def __init__(self, log_file: str, mode: str = 'a', level: str = None, fmt: str = None):
|
def __init__(self, log_file: str, mode: str = 'a', level: str = None, fmt: str = None):
|
||||||
self.log, self.mod = log_file, mode
|
self.log, self.mod = log_file, mode
|
||||||
|
@ -549,6 +581,22 @@ class Logger:
|
||||||
self.c = self.logger.critical
|
self.c = self.logger.critical
|
||||||
|
|
||||||
|
|
||||||
|
class Setting(dict):
|
||||||
|
def __init__(self, setting_file: str, setting_default: dict):
|
||||||
|
self.data_file = os.path.abspath(setting_file)
|
||||||
|
super().__init__(getJson(self.data_file, setting_default))
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
if item in self:
|
||||||
|
return super().__getitem__(item)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
putJson(self.data_file, self)
|
||||||
|
|
||||||
|
|
||||||
class CustomLineEdit(QLineEdit):
|
class CustomLineEdit(QLineEdit):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -612,11 +660,14 @@ class MainWindow(QMainWindow):
|
||||||
def __init__(self, logger: Logger):
|
def __init__(self, logger: Logger):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.app_name = '蓝牙音频连接'
|
self.app_name = '蓝牙音频连接'
|
||||||
self.app_version = ('1.0.3', '20240815', 'zhaoyafan', 'zhaoyafan@foxmail.com', 'https://www.fanscloud.net/')
|
self.app_version = ('1.1.0', '20241030', 'zhaoyafan', 'zhaoyafan@foxmail.com', 'https://www.fanscloud.net/')
|
||||||
self.app_text = {'search': '正在搜索', 'connecting': '连接中...'}
|
self.app_text = {'search': '正在搜索', 'connecting': '连接中...'}
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
self.toast = ToastNotification()
|
||||||
|
self.setting = Setting(os.path.abspath(os.path.join(tempfile.gettempdir(), '%s.config' % self.md5(__file__)[:16])), {})
|
||||||
self.bluesoleil = None
|
self.bluesoleil = None
|
||||||
self.currentDevHandle = None
|
self.currentDevHandle = None
|
||||||
|
self.currentDevRssi = 0
|
||||||
self.setWindowTitle(self.app_name)
|
self.setWindowTitle(self.app_name)
|
||||||
self.setGeometry(0, 0, 600, 400)
|
self.setGeometry(0, 0, 600, 400)
|
||||||
self.statusBar()
|
self.statusBar()
|
||||||
|
@ -652,6 +703,12 @@ class MainWindow(QMainWindow):
|
||||||
self.edtBtName.setFixedSize(250, 36)
|
self.edtBtName.setFixedSize(250, 36)
|
||||||
m_layout_1.addWidget(self.edtBtName)
|
m_layout_1.addWidget(self.edtBtName)
|
||||||
|
|
||||||
|
# 信号强度
|
||||||
|
self.edtBtRssi = CustomLineEditNoPopup('')
|
||||||
|
self.edtBtRssi.setReadOnly(True)
|
||||||
|
self.edtBtRssi.setFixedSize(38, 36)
|
||||||
|
m_layout_1.addWidget(self.edtBtRssi)
|
||||||
|
|
||||||
# 断开连接
|
# 断开连接
|
||||||
self.butDisconnect = CustomPushButton('断开连接')
|
self.butDisconnect = CustomPushButton('断开连接')
|
||||||
self.butDisconnect.setFixedSize(64, 36)
|
self.butDisconnect.setFixedSize(64, 36)
|
||||||
|
@ -673,7 +730,7 @@ class MainWindow(QMainWindow):
|
||||||
# 地址输入
|
# 地址输入
|
||||||
self.edtBtAddr = CustomLineEdit('')
|
self.edtBtAddr = CustomLineEdit('')
|
||||||
self.edtBtAddr.setStyleSheet('QLineEdit {font-size: 24px; font-family: \'Microsoft YaHei\'; color: #000000; background-color: #FFFFEE;}')
|
self.edtBtAddr.setStyleSheet('QLineEdit {font-size: 24px; font-family: \'Microsoft YaHei\'; color: #000000; background-color: #FFFFEE;}')
|
||||||
self.edtBtAddr.setFixedSize(249, 36)
|
self.edtBtAddr.setFixedSize(294, 36)
|
||||||
self.edtBtAddr.returnPressed.connect(self.on_connect_bt_address)
|
self.edtBtAddr.returnPressed.connect(self.on_connect_bt_address)
|
||||||
m_layout_3.addWidget(self.edtBtAddr)
|
m_layout_3.addWidget(self.edtBtAddr)
|
||||||
|
|
||||||
|
@ -694,6 +751,15 @@ class MainWindow(QMainWindow):
|
||||||
self.show()
|
self.show()
|
||||||
self.init_bluesoleil()
|
self.init_bluesoleil()
|
||||||
|
|
||||||
|
self.connect_status_update_timer = QTimer()
|
||||||
|
self.connect_status_update_timer.timeout.connect(self.connect_status_update)
|
||||||
|
|
||||||
|
if (self.bluesoleil.isBluetoothActive() and self.setting['deviceHandle'] > 0) == 1:
|
||||||
|
self.in_connect_process()
|
||||||
|
self.currentDevHandle = self.setting['deviceHandle'] or 0
|
||||||
|
self.currentDevRssi = self.setting['deviceRssi'] or 0
|
||||||
|
self.connect_action_ui_update()
|
||||||
|
|
||||||
def init_bluesoleil(self):
|
def init_bluesoleil(self):
|
||||||
try:
|
try:
|
||||||
self.bluesoleil = BtSdk()
|
self.bluesoleil = BtSdk()
|
||||||
|
@ -724,6 +790,20 @@ class MainWindow(QMainWindow):
|
||||||
if (not _c) == 0:
|
if (not _c) == 0:
|
||||||
QMessageBox.information(self, '关于', "" 'version: %s, build: %s, author: %s, email: %s, site: %s' % (_c[0], _c[1], _c[2], _c[3], '<a href=\'%s\'>%s</a>' % (_c[4], _c[4])))
|
QMessageBox.information(self, '关于', "" 'version: %s, build: %s, author: %s, email: %s, site: %s' % (_c[0], _c[1], _c[2], _c[3], '<a href=\'%s\'>%s</a>' % (_c[4], _c[4])))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def md5(input_data):
|
||||||
|
if isinstance(input_data, bytes):
|
||||||
|
return hashlib.md5(input_data).hexdigest()
|
||||||
|
if isinstance(input_data, str):
|
||||||
|
return hashlib.md5(bytes(input_data, encoding='utf-8')).hexdigest()
|
||||||
|
md5_object = hashlib.md5()
|
||||||
|
while True:
|
||||||
|
data = input_data.read(io.DEFAULT_BUFFER_SIZE)
|
||||||
|
if data:
|
||||||
|
md5_object.update(data)
|
||||||
|
else:
|
||||||
|
return md5_object.hexdigest()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def path_expandvars(path):
|
def path_expandvars(path):
|
||||||
resolve = os.path.expandvars(path)
|
resolve = os.path.expandvars(path)
|
||||||
|
@ -770,6 +850,8 @@ class MainWindow(QMainWindow):
|
||||||
def in_connect_process(self):
|
def in_connect_process(self):
|
||||||
self.edtBtName.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #7BD136')
|
self.edtBtName.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #7BD136')
|
||||||
self.edtBtName.setText('')
|
self.edtBtName.setText('')
|
||||||
|
self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #7BD136')
|
||||||
|
self.edtBtRssi.setText('')
|
||||||
self.edtBtAddr.setText('')
|
self.edtBtAddr.setText('')
|
||||||
self.edtBtAddr.setEnabled(False)
|
self.edtBtAddr.setEnabled(False)
|
||||||
self.edtBtAddr.setFocus()
|
self.edtBtAddr.setFocus()
|
||||||
|
@ -779,6 +861,8 @@ class MainWindow(QMainWindow):
|
||||||
def un_connect_process(self):
|
def un_connect_process(self):
|
||||||
self.edtBtName.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #FDD391')
|
self.edtBtName.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #FDD391')
|
||||||
self.edtBtName.setText('')
|
self.edtBtName.setText('')
|
||||||
|
self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #F5F5F5')
|
||||||
|
self.edtBtRssi.setText('')
|
||||||
self.edtBtAddr.setText('')
|
self.edtBtAddr.setText('')
|
||||||
self.edtBtAddr.setEnabled(True)
|
self.edtBtAddr.setEnabled(True)
|
||||||
self.edtBtAddr.setFocus()
|
self.edtBtAddr.setFocus()
|
||||||
|
@ -786,8 +870,13 @@ class MainWindow(QMainWindow):
|
||||||
self.butManuSearch.setEnabled(True)
|
self.butManuSearch.setEnabled(True)
|
||||||
|
|
||||||
def on_disconnect(self):
|
def on_disconnect(self):
|
||||||
|
self.connect_status_update_timer.stop()
|
||||||
self.bluesoleil.cancelDeviceDiscovery()
|
self.bluesoleil.cancelDeviceDiscovery()
|
||||||
self.bluesoleil.removeAllDevices()
|
self.bluesoleil.removeAllDevices()
|
||||||
|
self.currentDevHandle = 0
|
||||||
|
self.currentDevRssi = 0
|
||||||
|
self.setting['deviceHandle'] = 0
|
||||||
|
self.setting['deviceRssi'] = 0
|
||||||
self.un_connect_process()
|
self.un_connect_process()
|
||||||
|
|
||||||
def on_autosearch(self):
|
def on_autosearch(self):
|
||||||
|
@ -826,22 +915,41 @@ class MainWindow(QMainWindow):
|
||||||
return None
|
return None
|
||||||
return self.connect_action_ui(text)
|
return self.connect_action_ui(text)
|
||||||
|
|
||||||
|
def connect_action_ui_update(self):
|
||||||
|
self.connect_status_update_timer.start(1500)
|
||||||
|
self.edtBtAddr.setText(str(self.bluesoleil.getDeviceAddr(self.currentDevHandle)).replace(':', ''))
|
||||||
|
self.edtBtName.setText(str(self.bluesoleil.getDeviceName(self.currentDevHandle)))
|
||||||
|
self.edtBtRssi.setText(str(self.currentDevRssi if self.currentDevRssi != -3 else ''))
|
||||||
|
if -3 == self.currentDevRssi:
|
||||||
|
self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #FFF3F3')
|
||||||
|
if -65 < self.currentDevRssi <= -5:
|
||||||
|
self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #73CD2A')
|
||||||
|
if -72 < self.currentDevRssi <= -65:
|
||||||
|
self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #F2B32D')
|
||||||
|
if -99 < self.currentDevRssi <= -72:
|
||||||
|
self.edtBtRssi.setStyleSheet('font-size: 16px; font-family: \'Microsoft YaHei\'; color: #303030; border: 1px solid #808080; font-weight: bold; background-color: #F25555')
|
||||||
|
|
||||||
def connect_action_ui(self, handle):
|
def connect_action_ui(self, handle):
|
||||||
if (handle != 0) == 1:
|
if (handle != 0) == 1:
|
||||||
self.in_connect_process()
|
self.in_connect_process()
|
||||||
self.setWindowTitle(self.app_text['connecting'])
|
self.setWindowTitle(self.app_text['connecting'])
|
||||||
if (not self.connect(handle)) == 1:
|
self.currentDevRssi = self.connect(handle) or 0
|
||||||
|
else:
|
||||||
|
self.currentDevRssi = 0
|
||||||
|
if (not self.currentDevRssi) == 1:
|
||||||
self.un_connect_process()
|
self.un_connect_process()
|
||||||
self.edtBtAddr.selectAll()
|
self.edtBtAddr.selectAll()
|
||||||
self.setWindowTitle(self.app_name)
|
self.setWindowTitle(self.app_name)
|
||||||
QMessageBox.critical(self, '提示', '连接失败')
|
QMessageBox.critical(self, '提示', '连接失败')
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
self.edtBtAddr.setText(str(self.bluesoleil.getDeviceAddr(self.currentDevHandle)).replace(':', ''))
|
self.connect_action_ui_update()
|
||||||
self.edtBtName.setText(str(self.bluesoleil.getDeviceName(self.currentDevHandle)))
|
|
||||||
self.setWindowTitle(self.app_name)
|
self.setWindowTitle(self.app_name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def connect_status_update(self):
|
||||||
|
self.bluesoleil.isBluetoothActive() or self.on_disconnect()
|
||||||
|
|
||||||
def connect(self, data):
|
def connect(self, data):
|
||||||
devHandle = data
|
devHandle = data
|
||||||
if (isinstance(data, str)) == 1:
|
if (isinstance(data, str)) == 1:
|
||||||
|
@ -853,11 +961,23 @@ class MainWindow(QMainWindow):
|
||||||
break
|
break
|
||||||
if (not devHandle) == 1:
|
if (not devHandle) == 1:
|
||||||
return None
|
return None
|
||||||
|
rssi = 0
|
||||||
|
for i in range(8):
|
||||||
|
time.sleep(0.25)
|
||||||
|
rssi = self.bluesoleil.getDeviceRSSI(devHandle)
|
||||||
|
if (rssi != 0) == 1:
|
||||||
|
break
|
||||||
code = self.bluesoleil.connectAudioService(devHandle)
|
code = self.bluesoleil.connectAudioService(devHandle)
|
||||||
if (not code) == 1:
|
if (not code) == 1:
|
||||||
return None
|
return None
|
||||||
self.currentDevHandle = devHandle
|
self.currentDevHandle = devHandle
|
||||||
return True
|
self.setting['deviceHandle'] = devHandle
|
||||||
|
self.setting['deviceRssi'] = rssi
|
||||||
|
if (not rssi and code > 0) == 1:
|
||||||
|
return -3
|
||||||
|
else:
|
||||||
|
return rssi
|
||||||
|
|
||||||
|
|
||||||
class ManuWindow(QDialog):
|
class ManuWindow(QDialog):
|
||||||
def __init__(self, mainwindow):
|
def __init__(self, mainwindow):
|
||||||
|
@ -938,10 +1058,11 @@ class ManuWindow(QDialog):
|
||||||
self.mainwindow.bluesoleil.enableDeviceDiscovery()
|
self.mainwindow.bluesoleil.enableDeviceDiscovery()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.close()
|
self.close()
|
||||||
|
self.mainwindow.toast.show_toast('正在搜索设备')
|
||||||
timer = QTimer(self)
|
timer = QTimer(self)
|
||||||
timer.timeout.connect(self.task_end_search)
|
timer.timeout.connect(self.task_end_search)
|
||||||
timer.setSingleShot(True)
|
timer.setSingleShot(True)
|
||||||
timer.start(7000)
|
timer.start(9999)
|
||||||
|
|
||||||
def on_connect(self, data):
|
def on_connect(self, data):
|
||||||
self.mainwindow.bluesoleil.cancelDeviceDiscovery()
|
self.mainwindow.bluesoleil.cancelDeviceDiscovery()
|
||||||
|
@ -956,6 +1077,46 @@ class ManuWindow(QDialog):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ToastNotification(QDialog):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowType.Tool)
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setContentsMargins(5, 705, 5, 5)
|
||||||
|
|
||||||
|
self.label = QLabel("")
|
||||||
|
self.label.setStyleSheet("font-family: 'Microsoft YaHei'; font-size: 24px; color: #ffffff;")
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
self.setLayout(layout)
|
||||||
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||||
|
self.setStyleSheet("background-color: rgba(65, 65, 65, 180); border-radius: 5px; padding: 10px 16px 10px 16px")
|
||||||
|
|
||||||
|
self.adjustSize()
|
||||||
|
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.setInterval(2000)
|
||||||
|
self.timer.timeout.connect(self.close)
|
||||||
|
|
||||||
|
self.fade_animation = QPropertyAnimation(self, b"windowOpacity")
|
||||||
|
self.fade_animation.setDuration(500)
|
||||||
|
|
||||||
|
def show_toast(self, message):
|
||||||
|
self.close()
|
||||||
|
self.label.setText(message)
|
||||||
|
self.timer.isActive() and self.timer.stop()
|
||||||
|
self.timer.start()
|
||||||
|
self.adjustSize()
|
||||||
|
self.fade_animation.setStartValue(0.0)
|
||||||
|
self.fade_animation.setEndValue(1.0)
|
||||||
|
self.show()
|
||||||
|
self.fade_animation.start()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if self.timer.isActive():
|
||||||
|
self.timer.stop()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if (os.path.basename(__file__).lower().endswith('.int')) == 1:
|
if (os.path.basename(__file__).lower().endswith('.int')) == 1:
|
||||||
QCoreApplication.addLibraryPath(os.path.abspath(os.path.join(os.path.dirname(__file__), 'site-packages/PyQt5/Qt5/plugins')))
|
QCoreApplication.addLibraryPath(os.path.abspath(os.path.join(os.path.dirname(__file__), 'site-packages/PyQt5/Qt5/plugins')))
|
||||||
|
|
Loading…
Reference in New Issue