V3.0.0 — Rotation, Annuler/Refaire, Delivretiser, Trim&Shift, Inverser
- Rotation de pages : clic droit sur vignette → Pivoter +90/-90/180 Aussi dans le menu Pages - Ctrl+Z / Ctrl+Y : annuler et refaire (pile de 20 etats) - Delivretiser : coupe les pages en 2 et remet dans l'ordre sequentiel (inverse de la creation de livret) - Trim & Shift : decaler le contenu des pages - Decalage X/Y different pages paires/impaires - Compensation de creep progressive pour livrets epais - Inverser l'ordre des pages (menu Pages) - Fix impression : ouvre le PDF dans le lecteur par defaut - Barre outils droite : ajout Delivretiser, Trim/Shift, Inverser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
app/dialogs/trim_shift_dialog.py
Normal file
84
app/dialogs/trim_shift_dialog.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Dialogue Trim & Shift."""
|
||||
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QGroupBox, QDoubleSpinBox, QCheckBox,
|
||||
QDialogButtonBox)
|
||||
from app.engine.pdf_utils import mm_to_pt
|
||||
|
||||
|
||||
class TrimShiftDialog(QDialog):
|
||||
def __init__(self, total_pages=1, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Trim && Shift — Decaler le contenu")
|
||||
self.setMinimumWidth(420)
|
||||
self._total_pages = total_pages
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Decalage pages impaires
|
||||
grp_odd = QGroupBox("Decalage pages impaires (mm)")
|
||||
ol = QHBoxLayout(grp_odd)
|
||||
ol.addWidget(QLabel("X :"))
|
||||
self.spin_x = QDoubleSpinBox()
|
||||
self.spin_x.setRange(-200, 200)
|
||||
self.spin_x.setSuffix(" mm")
|
||||
ol.addWidget(self.spin_x)
|
||||
ol.addWidget(QLabel("Y :"))
|
||||
self.spin_y = QDoubleSpinBox()
|
||||
self.spin_y.setRange(-200, 200)
|
||||
self.spin_y.setSuffix(" mm")
|
||||
ol.addWidget(self.spin_y)
|
||||
layout.addWidget(grp_odd)
|
||||
|
||||
# Pages paires differentes
|
||||
self.chk_different = QCheckBox("Decalage different pour pages paires")
|
||||
layout.addWidget(self.chk_different)
|
||||
|
||||
grp_even = QGroupBox("Decalage pages paires (mm)")
|
||||
el = QHBoxLayout(grp_even)
|
||||
el.addWidget(QLabel("X :"))
|
||||
self.spin_x_even = QDoubleSpinBox()
|
||||
self.spin_x_even.setRange(-200, 200)
|
||||
self.spin_x_even.setSuffix(" mm")
|
||||
self.spin_x_even.setEnabled(False)
|
||||
el.addWidget(self.spin_x_even)
|
||||
el.addWidget(QLabel("Y :"))
|
||||
self.spin_y_even = QDoubleSpinBox()
|
||||
self.spin_y_even.setRange(-200, 200)
|
||||
self.spin_y_even.setSuffix(" mm")
|
||||
self.spin_y_even.setEnabled(False)
|
||||
el.addWidget(self.spin_y_even)
|
||||
layout.addWidget(grp_even)
|
||||
|
||||
self.chk_different.toggled.connect(self.spin_x_even.setEnabled)
|
||||
self.chk_different.toggled.connect(self.spin_y_even.setEnabled)
|
||||
|
||||
# Creep
|
||||
grp_creep = QGroupBox("Compensation de creep (livrets)")
|
||||
cl = QHBoxLayout(grp_creep)
|
||||
cl.addWidget(QLabel("Creep max :"))
|
||||
self.spin_creep = QDoubleSpinBox()
|
||||
self.spin_creep.setRange(0, 20)
|
||||
self.spin_creep.setSuffix(" mm")
|
||||
self.spin_creep.setToolTip("Decalage progressif pour compenser l'epaisseur du papier")
|
||||
cl.addWidget(self.spin_creep)
|
||||
layout.addWidget(grp_creep)
|
||||
|
||||
buttons = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def get_settings(self):
|
||||
return {
|
||||
"shift_x": mm_to_pt(self.spin_x.value()),
|
||||
"shift_y": mm_to_pt(self.spin_y.value()),
|
||||
"shift_x_even": mm_to_pt(self.spin_x_even.value()) if self.chk_different.isChecked() else None,
|
||||
"shift_y_even": mm_to_pt(self.spin_y_even.value()) if self.chk_different.isChecked() else None,
|
||||
"creep": mm_to_pt(self.spin_creep.value()),
|
||||
"creep_pages": self._total_pages,
|
||||
}
|
||||
59
app/engine/trim_shift.py
Normal file
59
app/engine/trim_shift.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Moteur Trim & Shift — decaler le contenu des pages."""
|
||||
|
||||
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
|
||||
from .pdf_utils import get_page_size
|
||||
|
||||
|
||||
def trim_and_shift(input_path, output_path, shift_x=0, shift_y=0,
|
||||
shift_x_even=None, shift_y_even=None,
|
||||
creep=0, creep_pages=0):
|
||||
"""Decaler le contenu des pages.
|
||||
|
||||
shift_x, shift_y: decalage en points (pages impaires si even specifie)
|
||||
shift_x_even, shift_y_even: decalage pour pages paires (None = meme que impaires)
|
||||
creep: decalage progressif en points (compensation epaisseur papier)
|
||||
creep_pages: nombre total de pages pour le calcul du creep
|
||||
"""
|
||||
reader = PdfReader(input_path)
|
||||
writer = PdfWriter()
|
||||
num_pages = len(reader.pages)
|
||||
|
||||
if shift_x_even is None:
|
||||
shift_x_even = shift_x
|
||||
if shift_y_even is None:
|
||||
shift_y_even = shift_y
|
||||
|
||||
for i, page in enumerate(reader.pages):
|
||||
page_num = i + 1
|
||||
w, h = get_page_size(page)
|
||||
|
||||
# Decalage de base
|
||||
if page_num % 2 == 0:
|
||||
dx, dy = shift_x_even, shift_y_even
|
||||
else:
|
||||
dx, dy = shift_x, shift_y
|
||||
|
||||
# Creep progressif (pour livrets)
|
||||
if creep != 0 and creep_pages > 0:
|
||||
# Les pages du centre se decalent le plus
|
||||
mid = num_pages / 2
|
||||
distance_from_center = abs(i - mid) / mid # 0 au centre, 1 aux bords
|
||||
creep_amount = creep * (1 - distance_from_center)
|
||||
if page_num % 2 == 0:
|
||||
dx -= creep_amount # pages paires : decaler vers la gauche
|
||||
else:
|
||||
dx += creep_amount # pages impaires : decaler vers la droite
|
||||
|
||||
if dx != 0 or dy != 0:
|
||||
new_page = PageObject.create_blank_page(width=w, height=h)
|
||||
overlay = PageObject.create_blank_page(width=w, height=h)
|
||||
overlay.merge_page(page)
|
||||
overlay.add_transformation(Transformation().translate(dx, dy))
|
||||
new_page.merge_page(overlay)
|
||||
writer.add_page(new_page)
|
||||
else:
|
||||
writer.add_page(page)
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
writer.write(f)
|
||||
return output_path
|
||||
73
app/engine/unbooklet.py
Normal file
73
app/engine/unbooklet.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Moteur Delivretiser — convertir un livret en pages sequentielles."""
|
||||
|
||||
from pypdf import PdfReader, PdfWriter, PageObject, Transformation
|
||||
from .pdf_utils import get_page_size
|
||||
import math
|
||||
|
||||
|
||||
def unbooklet(input_path, output_path):
|
||||
"""Delivretiser un PDF en format livret.
|
||||
|
||||
Chaque page du livret contient 2 pages cote a cote.
|
||||
On les coupe en 2 et on les remet dans l'ordre sequentiel.
|
||||
"""
|
||||
reader = PdfReader(input_path)
|
||||
num_sheets = len(reader.pages)
|
||||
total_pages = num_sheets * 2
|
||||
|
||||
# Extraire toutes les demi-pages
|
||||
half_pages = [] # (sheet_index, "left" ou "right")
|
||||
for i in range(num_sheets):
|
||||
half_pages.append((i, "left"))
|
||||
half_pages.append((i, "right"))
|
||||
|
||||
# Ordre du livret (piqure a cheval) :
|
||||
# Feuille 0 recto : page N, page 1
|
||||
# Feuille 0 verso : page 2, page N-1
|
||||
# Feuille 1 recto : page N-2, page 3
|
||||
# etc.
|
||||
# On reconstruit l'ordre
|
||||
page_map = {} # page_number (1-based) -> (sheet_index, side)
|
||||
n = total_pages
|
||||
for i in range(num_sheets):
|
||||
if i % 2 == 0:
|
||||
# Recto : gauche = N - i, droite = i + 1
|
||||
page_map[n - i] = (i, "left")
|
||||
page_map[i + 1] = (i, "right")
|
||||
else:
|
||||
# Verso : gauche = i + 1, droite = N - i
|
||||
page_map[i + 1] = (i, "left")
|
||||
page_map[n - i] = (i, "right")
|
||||
|
||||
writer = PdfWriter()
|
||||
|
||||
for page_num in range(1, total_pages + 1):
|
||||
if page_num not in page_map:
|
||||
continue
|
||||
sheet_idx, side = page_map[page_num]
|
||||
sheet = reader.pages[sheet_idx]
|
||||
sheet_w, sheet_h = get_page_size(sheet)
|
||||
half_w = sheet_w / 2
|
||||
|
||||
# Creer une nouvelle page avec la moitie
|
||||
new_page = PageObject.create_blank_page(width=half_w, height=sheet_h)
|
||||
|
||||
if side == "left":
|
||||
# Copier la moitie gauche
|
||||
overlay = PageObject.create_blank_page(width=half_w, height=sheet_h)
|
||||
overlay.merge_page(sheet)
|
||||
overlay.mediabox.upper_right = (half_w, sheet_h)
|
||||
new_page.merge_page(overlay)
|
||||
else:
|
||||
# Copier la moitie droite
|
||||
overlay = PageObject.create_blank_page(width=half_w, height=sheet_h)
|
||||
overlay.merge_page(sheet)
|
||||
overlay.add_transformation(Transformation().translate(-half_w, 0))
|
||||
overlay.mediabox.upper_right = (half_w, sheet_h)
|
||||
new_page.merge_page(overlay)
|
||||
|
||||
writer.add_page(new_page)
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
writer.write(f)
|
||||
return output_path
|
||||
@@ -39,6 +39,9 @@ from app.engine.bleeds import define_bleeds
|
||||
from app.engine.crop_marks import add_crop_marks, remove_crop_marks
|
||||
from app.engine.crop import crop_pages
|
||||
from app.engine.add_content import add_text_to_pages, add_image_to_pages
|
||||
from app.engine.unbooklet import unbooklet
|
||||
from app.engine.trim_shift import trim_and_shift
|
||||
from app.dialogs.trim_shift_dialog import TrimShiftDialog
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
@@ -56,8 +59,10 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._current_file = None
|
||||
self._result_file = None
|
||||
self._original_file = None # fichier avant crop (pour recadrage non-destructif)
|
||||
self._crop_settings = {} # {page_index: {top, bottom, left, right} en pt}
|
||||
self._original_file = None
|
||||
self._crop_settings = {}
|
||||
self._undo_stack = [] # pile de fichiers pour annuler
|
||||
self._redo_stack = []
|
||||
|
||||
# Drag & drop
|
||||
self.setAcceptDrops(True)
|
||||
@@ -200,6 +205,11 @@ class MainWindow(QMainWindow):
|
||||
file_menu.addSeparator()
|
||||
self._add_action(file_menu, "Quitter", "Ctrl+Q", self.close)
|
||||
|
||||
# Edition
|
||||
edit_menu = menubar.addMenu("Edition")
|
||||
self._add_action(edit_menu, "Annuler", "Ctrl+Z", self._do_undo)
|
||||
self._add_action(edit_menu, "Refaire", "Ctrl+Y", self._do_redo)
|
||||
|
||||
# Imposition
|
||||
impose_menu = menubar.addMenu("Imposition")
|
||||
self._add_action(impose_menu, "Creer un livret", None, self._do_booklet)
|
||||
@@ -207,10 +217,18 @@ class MainWindow(QMainWindow):
|
||||
self._add_action(impose_menu, "Repetition (Step && Repeat)", None, self._do_step_repeat)
|
||||
self._add_action(impose_menu, "Imposition manuelle", None, self._do_manual_impose)
|
||||
self._add_action(impose_menu, "Joindre deux pages", None, self._do_join_pages)
|
||||
impose_menu.addSeparator()
|
||||
self._add_action(impose_menu, "Delivretiser", None, self._do_unbooklet)
|
||||
|
||||
# Pages
|
||||
pages_menu = menubar.addMenu("Pages")
|
||||
self._add_action(pages_menu, "Recadrer...", None, self._do_crop)
|
||||
self._add_action(pages_menu, "Trim && Shift...", None, self._do_trim_shift)
|
||||
pages_menu.addSeparator()
|
||||
self._add_action(pages_menu, "Pivoter +90", None, lambda: self._rotate_current(90))
|
||||
self._add_action(pages_menu, "Pivoter -90", None, lambda: self._rotate_current(-90))
|
||||
self._add_action(pages_menu, "Pivoter 180", None, lambda: self._rotate_current(180))
|
||||
self._add_action(pages_menu, "Inverser l'ordre des pages", None, self._do_reverse_pages)
|
||||
pages_menu.addSeparator()
|
||||
self._add_action(pages_menu, "Inserer des pages blanches", None, self._do_insert_blank)
|
||||
|
||||
@@ -284,6 +302,7 @@ class MainWindow(QMainWindow):
|
||||
# Section Imposition
|
||||
rtb.addWidget(self._make_section_label("IMPOSITION"))
|
||||
rtb.addAction("Livret", self._do_booklet)
|
||||
rtb.addAction("Delivretiser", self._do_unbooklet)
|
||||
rtb.addAction("N-Up", self._do_nup)
|
||||
rtb.addAction("Repetition", self._do_step_repeat)
|
||||
rtb.addAction("Manuel", self._do_manual_impose)
|
||||
@@ -293,7 +312,9 @@ class MainWindow(QMainWindow):
|
||||
# Section Pages
|
||||
rtb.addWidget(self._make_section_label("PAGES"))
|
||||
rtb.addAction("Recadrer", self._do_crop)
|
||||
rtb.addAction("Trim/Shift", self._do_trim_shift)
|
||||
rtb.addAction("Pg blanche", self._do_insert_blank)
|
||||
rtb.addAction("Inverser", self._do_reverse_pages)
|
||||
rtb.addAction("Fusionner", self._do_merge)
|
||||
rtb.addSeparator()
|
||||
|
||||
@@ -340,6 +361,7 @@ class MainWindow(QMainWindow):
|
||||
self._page_panel.page_delete_requested.connect(self._on_page_delete)
|
||||
self._page_panel.page_insert_requested.connect(self._on_page_insert)
|
||||
self._page_panel.pages_reordered.connect(self._on_pages_reordered)
|
||||
self._page_panel.page_rotate_requested.connect(self._on_page_rotate)
|
||||
splitter.addWidget(self._page_panel)
|
||||
|
||||
# Viewer PDF
|
||||
@@ -488,6 +510,12 @@ class MainWindow(QMainWindow):
|
||||
self._progress.setRange(0, 1)
|
||||
|
||||
def _show_result(self, output_path):
|
||||
# Sauvegarder l'etat actuel pour annulation
|
||||
if self._current_file:
|
||||
self._undo_stack.append(self._current_file)
|
||||
self._redo_stack.clear()
|
||||
if len(self._undo_stack) > 20:
|
||||
self._undo_stack.pop(0)
|
||||
self._result_file = output_path
|
||||
self._current_file = output_path
|
||||
self._viewer.load_pdf(output_path)
|
||||
@@ -496,6 +524,97 @@ class MainWindow(QMainWindow):
|
||||
self._status_label.setText("Traitement termine")
|
||||
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||
|
||||
def _do_undo(self):
|
||||
if not self._undo_stack:
|
||||
return
|
||||
self._redo_stack.append(self._current_file)
|
||||
prev = self._undo_stack.pop()
|
||||
self._current_file = prev
|
||||
self._result_file = prev
|
||||
self._viewer.load_pdf(prev)
|
||||
self._page_panel.load_thumbnails(self._viewer)
|
||||
self._status_label.setText("Annule")
|
||||
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||
|
||||
def _do_redo(self):
|
||||
if not self._redo_stack:
|
||||
return
|
||||
self._undo_stack.append(self._current_file)
|
||||
nxt = self._redo_stack.pop()
|
||||
self._current_file = nxt
|
||||
self._result_file = nxt
|
||||
self._viewer.load_pdf(nxt)
|
||||
self._page_panel.load_thumbnails(self._viewer)
|
||||
self._status_label.setText("Refait")
|
||||
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||
|
||||
# ── Rotation ──
|
||||
|
||||
def _on_page_rotate(self, page_index, degrees):
|
||||
if not self._current_file:
|
||||
return
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
reader = PdfReader(self._current_file)
|
||||
writer = PdfWriter()
|
||||
for i, page in enumerate(reader.pages):
|
||||
if i == page_index:
|
||||
page = page.rotate(degrees)
|
||||
writer.add_page(page)
|
||||
output = self._make_temp()
|
||||
with open(output, "wb") as f:
|
||||
writer.write(f)
|
||||
self._show_result(output)
|
||||
|
||||
def _rotate_current(self, degrees):
|
||||
if not self._require_file():
|
||||
return
|
||||
self._on_page_rotate(self._viewer.current_page, degrees)
|
||||
|
||||
def _do_reverse_pages(self):
|
||||
if not self._require_file():
|
||||
return
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
self._show_progress()
|
||||
reader = PdfReader(self._current_file)
|
||||
writer = PdfWriter()
|
||||
for page in reversed(reader.pages):
|
||||
writer.add_page(page)
|
||||
output = self._make_temp()
|
||||
with open(output, "wb") as f:
|
||||
writer.write(f)
|
||||
self._show_result(output)
|
||||
self._status_label.setText("Ordre des pages inverse")
|
||||
|
||||
def _do_unbooklet(self):
|
||||
if not self._require_file():
|
||||
return
|
||||
self._show_progress()
|
||||
output = self._make_temp()
|
||||
try:
|
||||
unbooklet(self._current_file, output)
|
||||
self._show_result(output)
|
||||
self._status_label.setText("Delivretisation terminee")
|
||||
except Exception as e:
|
||||
self._hide_progress()
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _do_trim_shift(self):
|
||||
if not self._require_file():
|
||||
return
|
||||
from pypdf import PdfReader
|
||||
reader = PdfReader(self._current_file)
|
||||
dlg = TrimShiftDialog(total_pages=len(reader.pages), parent=self)
|
||||
if dlg.exec():
|
||||
self._show_progress()
|
||||
settings = dlg.get_settings()
|
||||
output = self._make_temp()
|
||||
try:
|
||||
trim_and_shift(self._current_file, output, **settings)
|
||||
self._show_result(output)
|
||||
except Exception as e:
|
||||
self._hide_progress()
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
# ── Actions imposition ──
|
||||
|
||||
def _do_booklet(self):
|
||||
@@ -789,14 +908,20 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
try:
|
||||
source = self._result_file or self._current_file
|
||||
# Copier dans un fichier temp avec un nom lisible
|
||||
import shutil
|
||||
tmp_print = os.path.join(tempfile.gettempdir(), "copydev_print.pdf")
|
||||
shutil.copy2(source, tmp_print)
|
||||
if sys.platform == "win32":
|
||||
os.startfile(source, "print")
|
||||
import subprocess
|
||||
# Ouvrir avec le lecteur PDF par defaut (qui a un menu imprimer)
|
||||
subprocess.Popen(["cmd", "/c", "start", "", tmp_print])
|
||||
else:
|
||||
import subprocess
|
||||
subprocess.Popen(["lp", source])
|
||||
self._status_label.setText("Impression lancee...")
|
||||
subprocess.Popen(["xdg-open", tmp_print])
|
||||
self._status_label.setText("Fichier ouvert pour impression")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", f"Impossible d'imprimer :\n{e}")
|
||||
QMessageBox.critical(self, "Erreur", f"Impossible d'ouvrir pour impression :\n{e}")
|
||||
|
||||
def _do_crop_marks(self):
|
||||
if not self._require_file():
|
||||
|
||||
@@ -9,10 +9,11 @@ from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
||||
class PagePanel(QWidget):
|
||||
"""Panneau de vignettes avec drag & drop, suppression, insertion."""
|
||||
|
||||
page_selected = pyqtSignal(int) # page index cliquee
|
||||
pages_reordered = pyqtSignal(list) # nouvelle liste d'indices
|
||||
page_delete_requested = pyqtSignal(int) # demande suppression
|
||||
page_insert_requested = pyqtSignal(int) # demande insertion page blanche apres
|
||||
page_selected = pyqtSignal(int)
|
||||
pages_reordered = pyqtSignal(list)
|
||||
page_delete_requested = pyqtSignal(int)
|
||||
page_insert_requested = pyqtSignal(int)
|
||||
page_rotate_requested = pyqtSignal(int, int) # page_index, degrees (90, -90, 180)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -106,6 +107,10 @@ class PagePanel(QWidget):
|
||||
QMenu::item:selected { background: #e0f4ff; color: #006fa6; }
|
||||
""")
|
||||
|
||||
act_rot_right = menu.addAction("Pivoter +90°")
|
||||
act_rot_left = menu.addAction("Pivoter -90°")
|
||||
act_rot_180 = menu.addAction("Pivoter 180°")
|
||||
menu.addSeparator()
|
||||
act_insert = menu.addAction("Inserer une page blanche apres")
|
||||
act_delete = menu.addAction("Supprimer cette page")
|
||||
|
||||
@@ -114,3 +119,9 @@ class PagePanel(QWidget):
|
||||
self.page_delete_requested.emit(row)
|
||||
elif action == act_insert:
|
||||
self.page_insert_requested.emit(row)
|
||||
elif action == act_rot_right:
|
||||
self.page_rotate_requested.emit(row, 90)
|
||||
elif action == act_rot_left:
|
||||
self.page_rotate_requested.emit(row, -90)
|
||||
elif action == act_rot_180:
|
||||
self.page_rotate_requested.emit(row, 180)
|
||||
|
||||
@@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool"
|
||||
BRANCH = "master"
|
||||
|
||||
# Version actuelle (incrementee a chaque release)
|
||||
VERSION = "2.6.0"
|
||||
VERSION = "3.0.0"
|
||||
|
||||
|
||||
def get_remote_version():
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.0
|
||||
3.0.0
|
||||
|
||||
Reference in New Issue
Block a user