V3.3.0 — Edition contenu, actions CLI, menu contextuel enrichi
- Edition de contenu PDF (Annotations > Editer le contenu) : - Onglet Texte : liste le texte detecte, masquer des lignes - Onglet Images : liste les images, remplacer ou masquer - Actions en ligne de commande (--action booklet_a4|booklet_a3|nup_2x2|unbooklet) - Menu contextuel Windows enrichi : - Ouvrir, Livret A4, Livret A3, 2x2 par feuille, Delivretiser - Bouton Editer dans la barre d'outils droite Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
110
app/dialogs/edit_content_dialog.py
Normal file
110
app/dialogs/edit_content_dialog.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"""Dialogue Edition de contenu — texte et images existants."""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QGroupBox, QListWidget, QListWidgetItem,
|
||||||
|
QPushButton, QDialogButtonBox, QTextEdit,
|
||||||
|
QFileDialog, QTabWidget, QWidget, QMessageBox)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
|
||||||
|
class EditContentDialog(QDialog):
|
||||||
|
def __init__(self, text_items=None, image_items=None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Edition de contenu")
|
||||||
|
self.setMinimumSize(500, 400)
|
||||||
|
self._text_items = text_items or []
|
||||||
|
self._image_items = image_items or []
|
||||||
|
self._actions = [] # liste de {"type": "mask_text"|"replace_image", ...}
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
tabs = QTabWidget()
|
||||||
|
|
||||||
|
# Onglet Texte
|
||||||
|
text_tab = QWidget()
|
||||||
|
tl = QVBoxLayout(text_tab)
|
||||||
|
tl.addWidget(QLabel("Texte detecte sur la page (cliquez pour masquer) :"))
|
||||||
|
self._text_list = QListWidget()
|
||||||
|
for item in self._text_items:
|
||||||
|
li = QListWidgetItem(item["text"])
|
||||||
|
li.setData(Qt.ItemDataRole.UserRole, item)
|
||||||
|
self._text_list.addItem(li)
|
||||||
|
tl.addWidget(self._text_list)
|
||||||
|
|
||||||
|
btn_mask = QPushButton("Masquer le texte selectionne")
|
||||||
|
btn_mask.clicked.connect(self._mask_text)
|
||||||
|
tl.addWidget(btn_mask)
|
||||||
|
|
||||||
|
tabs.addTab(text_tab, "Texte")
|
||||||
|
|
||||||
|
# Onglet Images
|
||||||
|
img_tab = QWidget()
|
||||||
|
il = QVBoxLayout(img_tab)
|
||||||
|
il.addWidget(QLabel("Images detectees sur la page :"))
|
||||||
|
self._img_list = QListWidget()
|
||||||
|
for item in self._image_items:
|
||||||
|
li = QListWidgetItem(f"{item['name']} ({item['width']}x{item['height']})")
|
||||||
|
li.setData(Qt.ItemDataRole.UserRole, item)
|
||||||
|
self._img_list.addItem(li)
|
||||||
|
il.addWidget(self._img_list)
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
btn_replace = QPushButton("Remplacer par une image...")
|
||||||
|
btn_replace.clicked.connect(self._replace_image)
|
||||||
|
btn_layout.addWidget(btn_replace)
|
||||||
|
btn_delete = QPushButton("Masquer cette image")
|
||||||
|
btn_delete.clicked.connect(self._delete_image)
|
||||||
|
btn_layout.addWidget(btn_delete)
|
||||||
|
il.addLayout(btn_layout)
|
||||||
|
|
||||||
|
tabs.addTab(img_tab, "Images")
|
||||||
|
|
||||||
|
layout.addWidget(tabs)
|
||||||
|
|
||||||
|
# Actions en attente
|
||||||
|
self._actions_label = QLabel("Actions en attente : 0")
|
||||||
|
layout.addWidget(self._actions_label)
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
buttons.accepted.connect(self.accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
def _mask_text(self):
|
||||||
|
item = self._text_list.currentItem()
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
data = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
self._actions.append({"type": "mask_text", "text": data["text"], "line_index": data["line_index"]})
|
||||||
|
item.setForeground(Qt.GlobalColor.red)
|
||||||
|
item.setText(f"[MASQUE] {data['text']}")
|
||||||
|
self._actions_label.setText(f"Actions en attente : {len(self._actions)}")
|
||||||
|
|
||||||
|
def _replace_image(self):
|
||||||
|
item = self._img_list.currentItem()
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
path, _ = QFileDialog.getOpenFileName(self, "Image de remplacement", "",
|
||||||
|
"Images (*.png *.jpg *.jpeg *.bmp)")
|
||||||
|
if path:
|
||||||
|
data = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
self._actions.append({"type": "replace_image", "name": data["name"], "new_path": path})
|
||||||
|
item.setText(f"[REMPLACE] {data['name']}")
|
||||||
|
self._actions_label.setText(f"Actions en attente : {len(self._actions)}")
|
||||||
|
|
||||||
|
def _delete_image(self):
|
||||||
|
item = self._img_list.currentItem()
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
data = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
self._actions.append({"type": "mask_image", "name": data["name"]})
|
||||||
|
item.setForeground(Qt.GlobalColor.red)
|
||||||
|
item.setText(f"[MASQUE] {data['name']}")
|
||||||
|
self._actions_label.setText(f"Actions en attente : {len(self._actions)}")
|
||||||
|
|
||||||
|
def get_actions(self):
|
||||||
|
return self._actions
|
||||||
90
app/engine/extract_content.py
Normal file
90
app/engine/extract_content.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""Extraction de texte et images depuis un PDF."""
|
||||||
|
|
||||||
|
from pypdf import PdfReader
|
||||||
|
from .pdf_utils import get_page_size
|
||||||
|
|
||||||
|
|
||||||
|
def extract_text_from_page(input_path, page_index):
|
||||||
|
"""Extraire le texte d'une page avec positions approximatives."""
|
||||||
|
reader = PdfReader(input_path)
|
||||||
|
if page_index >= len(reader.pages):
|
||||||
|
return []
|
||||||
|
page = reader.pages[page_index]
|
||||||
|
text = page.extract_text() or ""
|
||||||
|
lines = text.split("\n")
|
||||||
|
w, h = get_page_size(page)
|
||||||
|
# Positions approximatives (pypdf ne donne pas les coords exactes)
|
||||||
|
results = []
|
||||||
|
line_height = h / max(len(lines), 1)
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip():
|
||||||
|
results.append({
|
||||||
|
"text": line.strip(),
|
||||||
|
"y_approx": h - (i + 1) * line_height,
|
||||||
|
"line_index": i,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def extract_images_from_page(input_path, page_index):
|
||||||
|
"""Extraire les images d'une page."""
|
||||||
|
reader = PdfReader(input_path)
|
||||||
|
if page_index >= len(reader.pages):
|
||||||
|
return []
|
||||||
|
page = reader.pages[page_index]
|
||||||
|
images = []
|
||||||
|
if "/XObject" in page["/Resources"]:
|
||||||
|
xobjects = page["/Resources"]["/XObject"].get_object()
|
||||||
|
for obj_name in xobjects:
|
||||||
|
obj = xobjects[obj_name].get_object()
|
||||||
|
if obj["/Subtype"] == "/Image":
|
||||||
|
width = obj["/Width"]
|
||||||
|
height = obj["/Height"]
|
||||||
|
images.append({
|
||||||
|
"name": obj_name,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
})
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
def replace_image_in_page(input_path, output_path, page_index, image_name, new_image_path):
|
||||||
|
"""Remplacer une image dans une page (overlay avec masquage)."""
|
||||||
|
from pypdf import PdfWriter
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
from reportlab.lib.utils import ImageReader
|
||||||
|
import io
|
||||||
|
|
||||||
|
reader = PdfReader(input_path)
|
||||||
|
writer = PdfWriter()
|
||||||
|
|
||||||
|
for i, page in enumerate(reader.pages):
|
||||||
|
if i == page_index and new_image_path:
|
||||||
|
w, h = get_page_size(page)
|
||||||
|
# On ne peut pas vraiment remplacer l'image dans le flux PDF
|
||||||
|
# On ajoute la nouvelle image par-dessus (avec un masque blanc si besoin)
|
||||||
|
packet = io.BytesIO()
|
||||||
|
c = canvas.Canvas(packet, pagesize=(w, h))
|
||||||
|
try:
|
||||||
|
img = ImageReader(new_image_path)
|
||||||
|
img_w, img_h = img.getSize()
|
||||||
|
# Centrer l'image
|
||||||
|
scale = min(w / img_w, h / img_h) * 0.8
|
||||||
|
draw_w = img_w * scale
|
||||||
|
draw_h = img_h * scale
|
||||||
|
x = (w - draw_w) / 2
|
||||||
|
y = (h - draw_h) / 2
|
||||||
|
c.drawImage(new_image_path, x, y, width=draw_w, height=draw_h,
|
||||||
|
preserveAspectRatio=True, mask='auto')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
c.showPage()
|
||||||
|
c.save()
|
||||||
|
packet.seek(0)
|
||||||
|
overlay = PdfReader(packet)
|
||||||
|
page.merge_page(overlay.pages[0])
|
||||||
|
writer.add_page(page)
|
||||||
|
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
return output_path
|
||||||
@@ -41,7 +41,9 @@ from app.engine.crop import crop_pages
|
|||||||
from app.engine.add_content import add_text_to_pages, add_image_to_pages
|
from app.engine.add_content import add_text_to_pages, add_image_to_pages
|
||||||
from app.engine.unbooklet import unbooklet
|
from app.engine.unbooklet import unbooklet
|
||||||
from app.engine.trim_shift import trim_and_shift
|
from app.engine.trim_shift import trim_and_shift
|
||||||
|
from app.engine.extract_content import extract_text_from_page, extract_images_from_page
|
||||||
from app.dialogs.trim_shift_dialog import TrimShiftDialog
|
from app.dialogs.trim_shift_dialog import TrimShiftDialog
|
||||||
|
from app.dialogs.edit_content_dialog import EditContentDialog
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
@@ -236,6 +238,8 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Annotations
|
# Annotations
|
||||||
annot_menu = menubar.addMenu("Annotations")
|
annot_menu = menubar.addMenu("Annotations")
|
||||||
|
self._add_action(annot_menu, "Editer le contenu...", None, self._do_edit_content)
|
||||||
|
annot_menu.addSeparator()
|
||||||
self._add_action(annot_menu, "Ajouter du texte...", None, self._do_add_text)
|
self._add_action(annot_menu, "Ajouter du texte...", None, self._do_add_text)
|
||||||
self._add_action(annot_menu, "Ajouter une image...", None, self._do_add_image)
|
self._add_action(annot_menu, "Ajouter une image...", None, self._do_add_image)
|
||||||
annot_menu.addSeparator()
|
annot_menu.addSeparator()
|
||||||
@@ -331,6 +335,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Section Annotations
|
# Section Annotations
|
||||||
rtb.addWidget(self._make_section_label("ANNOTATIONS"))
|
rtb.addWidget(self._make_section_label("ANNOTATIONS"))
|
||||||
|
rtb.addAction("Editer", self._do_edit_content)
|
||||||
rtb.addAction("Texte", self._do_add_text)
|
rtb.addAction("Texte", self._do_add_text)
|
||||||
rtb.addAction("Image", self._do_add_image)
|
rtb.addAction("Image", self._do_add_image)
|
||||||
rtb.addAction("Numeros", self._do_page_numbers)
|
rtb.addAction("Numeros", self._do_page_numbers)
|
||||||
@@ -812,6 +817,72 @@ class MainWindow(QMainWindow):
|
|||||||
self._hide_progress()
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
|
def _do_edit_content(self):
|
||||||
|
if not self._require_file():
|
||||||
|
return
|
||||||
|
cur = self._viewer.current_page
|
||||||
|
texts = extract_text_from_page(self._current_file, cur)
|
||||||
|
images = extract_images_from_page(self._current_file, cur)
|
||||||
|
dlg = EditContentDialog(text_items=texts, image_items=images, parent=self)
|
||||||
|
if dlg.exec():
|
||||||
|
actions = dlg.get_actions()
|
||||||
|
if not actions:
|
||||||
|
return
|
||||||
|
self._show_progress()
|
||||||
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
from app.engine.pdf_utils import get_page_size
|
||||||
|
from reportlab.pdfgen import canvas as rl_canvas
|
||||||
|
import io
|
||||||
|
reader = PdfReader(self._current_file)
|
||||||
|
writer = PdfWriter()
|
||||||
|
page = reader.pages[cur]
|
||||||
|
w, h = get_page_size(page)
|
||||||
|
|
||||||
|
# Appliquer les masquages de texte
|
||||||
|
mask_lines = [a["line_index"] for a in actions if a["type"] == "mask_text"]
|
||||||
|
if mask_lines:
|
||||||
|
packet = io.BytesIO()
|
||||||
|
c = rl_canvas.Canvas(packet, pagesize=(w, h))
|
||||||
|
c.setFillColorRGB(1, 1, 1)
|
||||||
|
line_h = h / max(len(texts), 1)
|
||||||
|
for li in mask_lines:
|
||||||
|
y = h - (li + 1) * line_h
|
||||||
|
c.rect(0, y, w, line_h, fill=1, stroke=0)
|
||||||
|
c.showPage()
|
||||||
|
c.save()
|
||||||
|
packet.seek(0)
|
||||||
|
overlay = PdfReader(packet)
|
||||||
|
page.merge_page(overlay.pages[0])
|
||||||
|
|
||||||
|
# Remplacer les images
|
||||||
|
for a in actions:
|
||||||
|
if a["type"] == "replace_image" and a.get("new_path"):
|
||||||
|
from app.engine.extract_content import replace_image_in_page
|
||||||
|
# On fait un write intermediaire
|
||||||
|
tmp = self._make_temp()
|
||||||
|
for ii, pp in enumerate(reader.pages):
|
||||||
|
writer.add_page(pp)
|
||||||
|
with open(tmp, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
replace_image_in_page(tmp, tmp, cur, a["name"], a["new_path"])
|
||||||
|
reader = PdfReader(tmp)
|
||||||
|
writer = PdfWriter()
|
||||||
|
for pp in reader.pages:
|
||||||
|
writer.add_page(pp)
|
||||||
|
output = self._make_temp()
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
self._show_result(output)
|
||||||
|
return
|
||||||
|
|
||||||
|
for i, p in enumerate(reader.pages):
|
||||||
|
writer.add_page(p)
|
||||||
|
output = self._make_temp()
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
self._show_result(output)
|
||||||
|
self._status_label.setText("Contenu edite")
|
||||||
|
|
||||||
def _do_add_text(self):
|
def _do_add_text(self):
|
||||||
if not self._require_file():
|
if not self._require_file():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool"
|
|||||||
BRANCH = "master"
|
BRANCH = "master"
|
||||||
|
|
||||||
# Version actuelle (incrementee a chaque release)
|
# Version actuelle (incrementee a chaque release)
|
||||||
VERSION = "3.2.0"
|
VERSION = "3.3.0"
|
||||||
|
|
||||||
|
|
||||||
def get_remote_version():
|
def get_remote_version():
|
||||||
|
|||||||
50
main.py
50
main.py
@@ -207,24 +207,64 @@ def main():
|
|||||||
from app.main_window import MainWindow
|
from app.main_window import MainWindow
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
|
|
||||||
# Ouvrir les fichiers passes en argument
|
# Gestion des arguments
|
||||||
files_to_open = [a for a in sys.argv[1:] if a.lower().endswith('.pdf') and os.path.exists(a)]
|
args = sys.argv[1:]
|
||||||
|
action = None
|
||||||
|
files_to_open = []
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
if args[i] == "--action" and i + 1 < len(args):
|
||||||
|
action = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
elif args[i].lower().endswith('.pdf') and os.path.exists(args[i]):
|
||||||
|
files_to_open.append(args[i])
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
if len(files_to_open) == 1:
|
if len(files_to_open) == 1:
|
||||||
window._open_pdf(files_to_open[0])
|
window._open_pdf(files_to_open[0])
|
||||||
elif len(files_to_open) > 1:
|
elif len(files_to_open) > 1:
|
||||||
# Plusieurs fichiers = fusionner
|
|
||||||
from pypdf import PdfWriter
|
from pypdf import PdfWriter
|
||||||
import tempfile
|
import tempfile as tf
|
||||||
writer = PdfWriter()
|
writer = PdfWriter()
|
||||||
for f in files_to_open:
|
for f in files_to_open:
|
||||||
writer.append(f)
|
writer.append(f)
|
||||||
fd, merged = tempfile.mkstemp(suffix=".pdf")
|
fd, merged = tf.mkstemp(suffix=".pdf")
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
with open(merged, "wb") as out:
|
with open(merged, "wb") as out:
|
||||||
writer.write(out)
|
writer.write(out)
|
||||||
window._open_pdf(merged)
|
window._open_pdf(merged)
|
||||||
window._status_label.setText(f"{len(files_to_open)} fichiers fusionnes")
|
window._status_label.setText(f"{len(files_to_open)} fichiers fusionnes")
|
||||||
|
|
||||||
|
# Actions en ligne de commande
|
||||||
|
if action and files_to_open:
|
||||||
|
from PyQt6.QtCore import QTimer
|
||||||
|
def run_cli_action():
|
||||||
|
from app.engine.booklet import create_booklet
|
||||||
|
from app.engine.nup import nup_pages
|
||||||
|
from app.engine.unbooklet import unbooklet
|
||||||
|
import tempfile as tf
|
||||||
|
fd, output = tf.mkstemp(suffix=".pdf")
|
||||||
|
os.close(fd)
|
||||||
|
src = files_to_open[0]
|
||||||
|
try:
|
||||||
|
if action == "booklet_a4":
|
||||||
|
create_booklet(src, output, binding="saddle_stitch", sheet_size=(841.89, 595.28))
|
||||||
|
elif action == "booklet_a3":
|
||||||
|
create_booklet(src, output, binding="saddle_stitch", sheet_size=(1190.55, 841.89))
|
||||||
|
elif action == "nup_2x2":
|
||||||
|
nup_pages(src, output, cols=2, rows=2, sheet_size=(841.89, 595.28))
|
||||||
|
elif action == "unbooklet":
|
||||||
|
unbooklet(src, output)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
window._show_result(output)
|
||||||
|
window._status_label.setText(f"Action '{action}' executee")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(window, "Erreur", str(e))
|
||||||
|
QTimer.singleShot(500, run_cli_action)
|
||||||
|
|
||||||
window.show()
|
window.show()
|
||||||
splash.close()
|
splash.close()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\s
|
|||||||
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\bookletA3" /ve /d "Livret A3" /f >nul 2>&1
|
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\bookletA3" /ve /d "Livret A3" /f >nul 2>&1
|
||||||
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\bookletA3\command" /ve /d "\"%EXE_PATH%\" --action booklet_a3 \"%%1\"" /f >nul 2>&1
|
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\bookletA3\command" /ve /d "\"%EXE_PATH%\" --action booklet_a3 \"%%1\"" /f >nul 2>&1
|
||||||
|
|
||||||
|
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\nup2x2" /ve /d "2x2 par feuille" /f >nul 2>&1
|
||||||
|
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\nup2x2\command" /ve /d "\"%EXE_PATH%\" --action nup_2x2 \"%%1\"" /f >nul 2>&1
|
||||||
|
|
||||||
|
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\unbooklet" /ve /d "Delivretiser" /f >nul 2>&1
|
||||||
|
reg add "HKCU\Software\Classes\SystemFileAssociations\.pdf\shell\CopyDevImpose\shell\unbooklet\command" /ve /d "\"%EXE_PATH%\" --action unbooklet \"%%1\"" /f >nul 2>&1
|
||||||
|
|
||||||
echo [OK] Menu contextuel ajoute
|
echo [OK] Menu contextuel ajoute
|
||||||
echo (clic droit sur PDF > CopyDev Impose > ...)
|
echo (clic droit sur PDF > CopyDev Impose > ...)
|
||||||
goto :EOF
|
goto :EOF
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3.2.0
|
3.3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user