pyshort-cli 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyshort_cli-1.1.0/MANIFEST.in +8 -0
- pyshort_cli-1.1.0/PKG-INFO +59 -0
- pyshort_cli-1.1.0/README.md +46 -0
- pyshort_cli-1.1.0/pyproject.toml +26 -0
- pyshort_cli-1.1.0/pyshort/__init__.py +1 -0
- pyshort_cli-1.1.0/pyshort/__main__.py +4 -0
- pyshort_cli-1.1.0/pyshort/api.py +31 -0
- pyshort_cli-1.1.0/pyshort/cli.py +151 -0
- pyshort_cli-1.1.0/pyshort/config.py +26 -0
- pyshort_cli-1.1.0/pyshort/gui.py +268 -0
- pyshort_cli-1.1.0/pyshort_cli.egg-info/SOURCES.txt +9 -0
- pyshort_cli-1.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyshort-cli
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Raccourcisseur d'URLs en ligne de commande
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: url,shortener,cli
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer>=0.12.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
12
|
+
Requires-Dist: rich>=13.7.0
|
|
13
|
+
|
|
14
|
+
# pyshort — Raccourcisseur d'URLs en ligne de commande
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install pyshort
|
|
20
|
+
# ou en local depuis ce dossier :
|
|
21
|
+
pip install -e .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration (une seule fois)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pyshort config --api-url https://mon-api.render.com
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Utilisation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Raccourcir une URL (résultat copié automatiquement)
|
|
34
|
+
pyshort shorten https://www.monsite.com/une/url/tres/longue
|
|
35
|
+
# ╭─ 🔗 URL raccourcie ─╮
|
|
36
|
+
# │ https://shr.ty/abc12x │
|
|
37
|
+
# │ ✅ Copié dans le presse-papier │
|
|
38
|
+
# ╰────────────────────────╯
|
|
39
|
+
|
|
40
|
+
# Sans copie automatique
|
|
41
|
+
pyshort shorten https://example.com --no-copy
|
|
42
|
+
|
|
43
|
+
# Voir les statistiques (code ou URL courte)
|
|
44
|
+
pyshort stats abc12x
|
|
45
|
+
pyshort stats https://shr.ty/abc12x
|
|
46
|
+
|
|
47
|
+
# Voir la config actuelle
|
|
48
|
+
pyshort config --show
|
|
49
|
+
|
|
50
|
+
# Version
|
|
51
|
+
pyshort version
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Lancer les tests
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install pytest
|
|
58
|
+
pytest tests/ -v
|
|
59
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# pyshort — Raccourcisseur d'URLs en ligne de commande
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install pyshort
|
|
7
|
+
# ou en local depuis ce dossier :
|
|
8
|
+
pip install -e .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration (une seule fois)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pyshort config --api-url https://mon-api.render.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Utilisation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Raccourcir une URL (résultat copié automatiquement)
|
|
21
|
+
pyshort shorten https://www.monsite.com/une/url/tres/longue
|
|
22
|
+
# ╭─ 🔗 URL raccourcie ─╮
|
|
23
|
+
# │ https://shr.ty/abc12x │
|
|
24
|
+
# │ ✅ Copié dans le presse-papier │
|
|
25
|
+
# ╰────────────────────────╯
|
|
26
|
+
|
|
27
|
+
# Sans copie automatique
|
|
28
|
+
pyshort shorten https://example.com --no-copy
|
|
29
|
+
|
|
30
|
+
# Voir les statistiques (code ou URL courte)
|
|
31
|
+
pyshort stats abc12x
|
|
32
|
+
pyshort stats https://shr.ty/abc12x
|
|
33
|
+
|
|
34
|
+
# Voir la config actuelle
|
|
35
|
+
pyshort config --show
|
|
36
|
+
|
|
37
|
+
# Version
|
|
38
|
+
pyshort version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Lancer les tests
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install pytest
|
|
45
|
+
pytest tests/ -v
|
|
46
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyshort-cli"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "Raccourcisseur d'URLs en ligne de commande"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["url", "shortener", "cli"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"typer>=0.12.0",
|
|
15
|
+
"httpx>=0.27.0",
|
|
16
|
+
"pyperclip>=1.8.2",
|
|
17
|
+
"rich>=13.7.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
pyshort = "pyshort.cli:app"
|
|
22
|
+
pyshort-gui = "pyshort.gui:launch_gui"
|
|
23
|
+
|
|
24
|
+
# On liste explicitement les packages à inclure — UNIQUEMENT pyshort/
|
|
25
|
+
[tool.setuptools.packages]
|
|
26
|
+
find = { include = ["pyshort", "pyshort.*"] }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from pyshort.config import get_api_url
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def shorten(url: str) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Appelle POST /shorten et retourne le dict de réponse.
|
|
9
|
+
Lève une exception httpx en cas d'erreur réseau ou HTTP.
|
|
10
|
+
"""
|
|
11
|
+
api_url = get_api_url()
|
|
12
|
+
response = httpx.post(
|
|
13
|
+
f"{api_url}/shorten",
|
|
14
|
+
json={"url": url},
|
|
15
|
+
timeout=10,
|
|
16
|
+
)
|
|
17
|
+
response.raise_for_status()
|
|
18
|
+
return response.json()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def stats(code: str) -> dict:
|
|
22
|
+
"""
|
|
23
|
+
Appelle GET /stats/{code} et retourne le dict de réponse.
|
|
24
|
+
"""
|
|
25
|
+
api_url = get_api_url()
|
|
26
|
+
response = httpx.get(
|
|
27
|
+
f"{api_url}/stats/{code}",
|
|
28
|
+
timeout=10,
|
|
29
|
+
)
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
return response.json()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from pyshort import __version__
|
|
10
|
+
from pyshort.api import shorten as api_shorten
|
|
11
|
+
from pyshort.api import stats as api_stats
|
|
12
|
+
from pyshort.config import get_api_url, save_api_url
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="pyshort",
|
|
16
|
+
help="🔗 Raccourcissez vos URLs directement depuis le terminal.",
|
|
17
|
+
add_completion=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _try_copy(text: str) -> bool:
|
|
24
|
+
"""Tente de copier le texte dans le presse-papier. Retourne True si succès."""
|
|
25
|
+
try:
|
|
26
|
+
import pyperclip
|
|
27
|
+
pyperclip.copy(text)
|
|
28
|
+
return True
|
|
29
|
+
except Exception:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ─────────────────────────────────────────────
|
|
34
|
+
# pyshort shorten <url>
|
|
35
|
+
# ─────────────────────────────────────────────
|
|
36
|
+
@app.command()
|
|
37
|
+
def shorten(
|
|
38
|
+
url: str = typer.Argument(..., help="L'URL longue à raccourcir."),
|
|
39
|
+
no_copy: bool = typer.Option(False, "--no-copy", help="Ne pas copier dans le presse-papier."),
|
|
40
|
+
):
|
|
41
|
+
"""Raccourcit une URL et copie le résultat dans le presse-papier."""
|
|
42
|
+
with console.status("[bold cyan]Raccourcissement en cours…[/]"):
|
|
43
|
+
try:
|
|
44
|
+
data = api_shorten(url)
|
|
45
|
+
except httpx.HTTPStatusError as e:
|
|
46
|
+
console.print(f"[bold red]Erreur HTTP {e.response.status_code}[/] : {e.response.text}")
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
except httpx.RequestError:
|
|
49
|
+
api = get_api_url()
|
|
50
|
+
console.print(f"[bold red]Impossible de joindre l'API[/] ({api})")
|
|
51
|
+
console.print("💡 Vérifiez que le serveur tourne ou configurez l'URL avec [bold]pyshort config[/]")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
short_url = data["short"]
|
|
55
|
+
|
|
56
|
+
copied = False
|
|
57
|
+
if not no_copy:
|
|
58
|
+
copied = _try_copy(short_url)
|
|
59
|
+
|
|
60
|
+
panel_content = f"[bold green]{short_url}[/]"
|
|
61
|
+
if copied:
|
|
62
|
+
panel_content += "\n[dim]✅ Copié dans le presse-papier[/]"
|
|
63
|
+
|
|
64
|
+
console.print(Panel(panel_content, title="🔗 URL raccourcie", border_style="green"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────────
|
|
68
|
+
# pyshort stats <code>
|
|
69
|
+
# ─────────────────────────────────────────────
|
|
70
|
+
@app.command()
|
|
71
|
+
def stats(
|
|
72
|
+
code: str = typer.Argument(..., help="Le code court (ex: abc12x) ou l'URL courte complète."),
|
|
73
|
+
):
|
|
74
|
+
"""Affiche les statistiques d'une URL raccourcie."""
|
|
75
|
+
# Accepte aussi bien "abc12x" que "https://shr.ty/abc12x"
|
|
76
|
+
if "/" in code:
|
|
77
|
+
code = code.rstrip("/").split("/")[-1]
|
|
78
|
+
|
|
79
|
+
with console.status("[bold cyan]Récupération des stats…[/]"):
|
|
80
|
+
try:
|
|
81
|
+
data = api_stats(code)
|
|
82
|
+
except httpx.HTTPStatusError as e:
|
|
83
|
+
if e.response.status_code == 404:
|
|
84
|
+
console.print(f"[bold red]Code introuvable :[/] '{code}'")
|
|
85
|
+
else:
|
|
86
|
+
console.print(f"[bold red]Erreur HTTP {e.response.status_code}[/]")
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
except httpx.RequestError:
|
|
89
|
+
api = get_api_url()
|
|
90
|
+
console.print(f"[bold red]Impossible de joindre l'API[/] ({api})")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
table = Table(show_header=False, border_style="cyan", min_width=50)
|
|
94
|
+
table.add_column("Clé", style="bold cyan", width=14)
|
|
95
|
+
table.add_column("Valeur")
|
|
96
|
+
|
|
97
|
+
# Formater la date
|
|
98
|
+
created = data["created_at"].replace("T", " ").split(".")[0]
|
|
99
|
+
|
|
100
|
+
table.add_row("🔑 Code", data["code"])
|
|
101
|
+
table.add_row("🔗 URL courte", f"[green]{data['short']}[/]")
|
|
102
|
+
table.add_row("📄 Originale", data["original"])
|
|
103
|
+
table.add_row("👆 Clics", str(data["clicks"]))
|
|
104
|
+
table.add_row("📅 Créé le", created)
|
|
105
|
+
|
|
106
|
+
console.print(table)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ─────────────────────────────────────────────
|
|
110
|
+
# pyshort config (commande cachée)
|
|
111
|
+
# ─────────────────────────────────────────────
|
|
112
|
+
@app.command(hidden=True)
|
|
113
|
+
def config(
|
|
114
|
+
api_url: Optional[str] = typer.Option(None, "--api-url", help="URL de l'API backend."),
|
|
115
|
+
show: bool = typer.Option(False, "--show", help="Affiche la configuration actuelle."),
|
|
116
|
+
):
|
|
117
|
+
"""Configure l'URL de l'API backend (sauvegardée dans ~/.pyshortrc)."""
|
|
118
|
+
if show or api_url is None:
|
|
119
|
+
current = get_api_url()
|
|
120
|
+
console.print(f"[bold]API URL :[/] {current}")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
save_api_url(api_url)
|
|
124
|
+
console.print(f"[bold green]✅ API URL mise à jour :[/] {api_url}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ─────────────────────────────────────────────
|
|
128
|
+
# pyshort version
|
|
129
|
+
# ─────────────────────────────────────────────
|
|
130
|
+
@app.command()
|
|
131
|
+
def version():
|
|
132
|
+
"""Affiche la version de pyshort."""
|
|
133
|
+
console.print(f"[bold]pyshort[/] v{__version__}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
app()
|
|
138
|
+
|
|
139
|
+
# ─────────────────────────────────────────────
|
|
140
|
+
# pyshort gui
|
|
141
|
+
# ─────────────────────────────────────────────
|
|
142
|
+
@app.command()
|
|
143
|
+
def gui():
|
|
144
|
+
"""Lance l'interface graphique (pour les utilisateurs non-développeurs)."""
|
|
145
|
+
try:
|
|
146
|
+
from pyshort.gui import launch_gui
|
|
147
|
+
console.print("[bold cyan]Ouverture de l'interface graphique…[/]")
|
|
148
|
+
launch_gui()
|
|
149
|
+
except ImportError:
|
|
150
|
+
console.print("[bold red]tkinter n'est pas disponible sur ce système.[/]")
|
|
151
|
+
console.print("Sur Ubuntu/Debian : [bold]sudo apt install python3-tk[/]")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
CONFIG_PATH = Path.home() / ".pyshortrc"
|
|
5
|
+
DEFAULT_API_URL = "http://localhost:8000"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_config() -> configparser.ConfigParser:
|
|
9
|
+
config = configparser.ConfigParser()
|
|
10
|
+
if CONFIG_PATH.exists():
|
|
11
|
+
config.read(CONFIG_PATH)
|
|
12
|
+
return config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_api_url() -> str:
|
|
16
|
+
config = load_config()
|
|
17
|
+
return config.get("default", "api_url", fallback=DEFAULT_API_URL)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def save_api_url(api_url: str) -> None:
|
|
21
|
+
config = load_config()
|
|
22
|
+
if "default" not in config:
|
|
23
|
+
config["default"] = {}
|
|
24
|
+
config["default"]["api_url"] = api_url
|
|
25
|
+
with open(CONFIG_PATH, "w") as f:
|
|
26
|
+
config.write(f)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from tkinter import font as tkfont
|
|
4
|
+
from tkinter import messagebox
|
|
5
|
+
|
|
6
|
+
from pyshort.api import shorten as api_shorten
|
|
7
|
+
from pyshort.api import stats as api_stats
|
|
8
|
+
from pyshort.config import get_api_url, save_api_url
|
|
9
|
+
|
|
10
|
+
# ── Couleurs ──────────────────────────────────────────────────────────────────
|
|
11
|
+
BG = "#0f172a"
|
|
12
|
+
CARD = "#1e293b"
|
|
13
|
+
BORDER = "#334155"
|
|
14
|
+
BLUE = "#0ea5e9"
|
|
15
|
+
BLUE_H = "#38bdf8"
|
|
16
|
+
TEXT = "#e2e8f0"
|
|
17
|
+
MUTED = "#64748b"
|
|
18
|
+
GREEN = "#22c55e"
|
|
19
|
+
RED_BG = "#450a0a"
|
|
20
|
+
RED_FG = "#fca5a5"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PyShortGUI:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.root = tk.Tk()
|
|
26
|
+
self.root.title("PyShort — Raccourcisseur d'URLs")
|
|
27
|
+
self.root.configure(bg=BG)
|
|
28
|
+
self.root.resizable(False, False)
|
|
29
|
+
|
|
30
|
+
# Centrer la fenêtre
|
|
31
|
+
w, h = 420, 520
|
|
32
|
+
self.root.geometry(f"{w}x{h}")
|
|
33
|
+
self.root.eval("tk::PlaceWindow . center")
|
|
34
|
+
|
|
35
|
+
self._build_ui()
|
|
36
|
+
|
|
37
|
+
# ── Construction de l'interface ───────────────────────────────────────────
|
|
38
|
+
def _build_ui(self):
|
|
39
|
+
# ── Header ────────────────────────────────────────────────────────────
|
|
40
|
+
header = tk.Frame(self.root, bg=CARD, pady=12)
|
|
41
|
+
header.pack(fill="x")
|
|
42
|
+
|
|
43
|
+
tk.Label(header, text="🔗 PyShort", bg=CARD, fg=BLUE,
|
|
44
|
+
font=("Segoe UI", 16, "bold")).pack(side="left", padx=16)
|
|
45
|
+
tk.Label(header, text="Raccourcisseur d'URLs", bg=CARD, fg=MUTED,
|
|
46
|
+
font=("Segoe UI", 10)).pack(side="left")
|
|
47
|
+
|
|
48
|
+
# ── Corps ─────────────────────────────────────────────────────────────
|
|
49
|
+
body = tk.Frame(self.root, bg=BG, padx=20, pady=20)
|
|
50
|
+
body.pack(fill="both", expand=True)
|
|
51
|
+
|
|
52
|
+
# Label URL
|
|
53
|
+
tk.Label(body, text="Collez ou tapez votre URL :", bg=BG, fg=MUTED,
|
|
54
|
+
font=("Segoe UI", 9, "bold")).pack(anchor="w", pady=(0, 6))
|
|
55
|
+
|
|
56
|
+
# Champ URL
|
|
57
|
+
url_frame = tk.Frame(body, bg=BORDER, padx=1, pady=1)
|
|
58
|
+
url_frame.pack(fill="x", pady=(0, 8))
|
|
59
|
+
|
|
60
|
+
self.url_var = tk.StringVar()
|
|
61
|
+
self.url_entry = tk.Entry(
|
|
62
|
+
url_frame, textvariable=self.url_var,
|
|
63
|
+
bg=CARD, fg=TEXT, insertbackground=TEXT,
|
|
64
|
+
font=("Segoe UI", 11), relief="flat",
|
|
65
|
+
bd=0,
|
|
66
|
+
)
|
|
67
|
+
self.url_entry.pack(fill="x", padx=10, pady=10)
|
|
68
|
+
self.url_entry.bind("<Return>", lambda e: self._shorten())
|
|
69
|
+
self.url_entry.focus()
|
|
70
|
+
|
|
71
|
+
# Bouton coller
|
|
72
|
+
self._btn_paste = self._make_button(
|
|
73
|
+
body, "📋 Coller depuis le presse-papier",
|
|
74
|
+
self._paste, bg=CARD, fg=MUTED, hover_bg=BORDER
|
|
75
|
+
)
|
|
76
|
+
self._btn_paste.pack(fill="x", pady=(0, 10))
|
|
77
|
+
|
|
78
|
+
# Bouton raccourcir
|
|
79
|
+
self._btn_shorten = self._make_button(
|
|
80
|
+
body, "🔗 Raccourcir l'URL",
|
|
81
|
+
self._shorten, bg=BLUE, fg="white", hover_bg=BLUE_H,
|
|
82
|
+
font_size=12, bold=True
|
|
83
|
+
)
|
|
84
|
+
self._btn_shorten.pack(fill="x", pady=(0, 16))
|
|
85
|
+
|
|
86
|
+
# Séparateur
|
|
87
|
+
tk.Frame(body, bg=BORDER, height=1).pack(fill="x", pady=(0, 16))
|
|
88
|
+
|
|
89
|
+
# Zone résultat
|
|
90
|
+
tk.Label(body, text="URL raccourcie :", bg=BG, fg=MUTED,
|
|
91
|
+
font=("Segoe UI", 9, "bold")).pack(anchor="w", pady=(0, 6))
|
|
92
|
+
|
|
93
|
+
result_frame = tk.Frame(body, bg=BORDER, padx=1, pady=1)
|
|
94
|
+
result_frame.pack(fill="x", pady=(0, 8))
|
|
95
|
+
|
|
96
|
+
self.result_var = tk.StringVar(value="")
|
|
97
|
+
result_inner = tk.Frame(result_frame, bg=CARD)
|
|
98
|
+
result_inner.pack(fill="x")
|
|
99
|
+
|
|
100
|
+
self.result_entry = tk.Entry(
|
|
101
|
+
result_inner, textvariable=self.result_var,
|
|
102
|
+
bg=CARD, fg=BLUE, font=("Segoe UI", 11),
|
|
103
|
+
relief="flat", bd=0, state="readonly",
|
|
104
|
+
readonlybackground=CARD, cursor="hand2",
|
|
105
|
+
)
|
|
106
|
+
self.result_entry.pack(fill="x", padx=10, pady=10)
|
|
107
|
+
|
|
108
|
+
# Bouton copier
|
|
109
|
+
self._btn_copy = self._make_button(
|
|
110
|
+
body, "📋 Copier dans le presse-papier",
|
|
111
|
+
self._copy, bg=CARD, fg=MUTED, hover_bg=BORDER
|
|
112
|
+
)
|
|
113
|
+
self._btn_copy.pack(fill="x", pady=(0, 8))
|
|
114
|
+
|
|
115
|
+
# Stats
|
|
116
|
+
self.stats_var = tk.StringVar(value="")
|
|
117
|
+
tk.Label(body, textvariable=self.stats_var, bg=BG, fg=MUTED,
|
|
118
|
+
font=("Segoe UI", 9)).pack(anchor="w")
|
|
119
|
+
|
|
120
|
+
# Message status (erreur ou succès)
|
|
121
|
+
self.status_var = tk.StringVar(value="")
|
|
122
|
+
self.status_label = tk.Label(
|
|
123
|
+
body, textvariable=self.status_var,
|
|
124
|
+
bg=BG, fg=RED_FG, font=("Segoe UI", 9),
|
|
125
|
+
wraplength=380, justify="left"
|
|
126
|
+
)
|
|
127
|
+
self.status_label.pack(anchor="w", pady=(8, 0))
|
|
128
|
+
|
|
129
|
+
# ── Footer ────────────────────────────────────────────────────────────
|
|
130
|
+
footer = tk.Frame(self.root, bg=CARD, pady=8)
|
|
131
|
+
footer.pack(fill="x", side="bottom")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── Actions ───────────────────────────────────────────────────────────────
|
|
136
|
+
def _paste(self):
|
|
137
|
+
try:
|
|
138
|
+
text = self.root.clipboard_get()
|
|
139
|
+
self.url_var.set(text.strip())
|
|
140
|
+
self.url_entry.focus()
|
|
141
|
+
except tk.TclError:
|
|
142
|
+
self._set_status("Presse-papier vide ou inaccessible.", error=True)
|
|
143
|
+
|
|
144
|
+
def _shorten(self):
|
|
145
|
+
url = self.url_var.get().strip()
|
|
146
|
+
if not url:
|
|
147
|
+
self._set_status("Veuillez entrer une URL.", error=True)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# Ajoute https:// si manquant
|
|
151
|
+
if not url.startswith("http"):
|
|
152
|
+
url = "https://" + url
|
|
153
|
+
self.url_var.set(url)
|
|
154
|
+
|
|
155
|
+
self._btn_shorten.config(text="⏳ Raccourcissement…", state="disabled")
|
|
156
|
+
self._set_status("")
|
|
157
|
+
self.result_var.set("")
|
|
158
|
+
self.stats_var.set("")
|
|
159
|
+
|
|
160
|
+
# Appel API dans un thread séparé pour ne pas bloquer l'UI
|
|
161
|
+
threading.Thread(target=self._do_shorten, args=(url,), daemon=True).start()
|
|
162
|
+
|
|
163
|
+
def _do_shorten(self, url):
|
|
164
|
+
try:
|
|
165
|
+
import httpx
|
|
166
|
+
data = api_shorten(url)
|
|
167
|
+
self.root.after(0, self._on_shorten_success, data)
|
|
168
|
+
except httpx.RequestError:
|
|
169
|
+
api = get_api_url()
|
|
170
|
+
self.root.after(0, self._set_status,
|
|
171
|
+
f"Impossible de joindre l'API ({api}).\nVérifiez que le serveur est lancé.", True)
|
|
172
|
+
except httpx.HTTPStatusError as e:
|
|
173
|
+
self.root.after(0, self._set_status,
|
|
174
|
+
f"Erreur {e.response.status_code} : URL invalide.", True)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self.root.after(0, self._set_status, str(e), True)
|
|
177
|
+
finally:
|
|
178
|
+
self.root.after(0, self._btn_shorten.config,
|
|
179
|
+
{"text": "🔗 Raccourcir l'URL", "state": "normal"})
|
|
180
|
+
|
|
181
|
+
def _on_shorten_success(self, data):
|
|
182
|
+
self.result_var.set(data["short"])
|
|
183
|
+
self._set_status("✅ URL raccourcie avec succès !", error=False)
|
|
184
|
+
self.status_label.config(fg=GREEN)
|
|
185
|
+
|
|
186
|
+
# Charger les stats
|
|
187
|
+
threading.Thread(target=self._load_stats, args=(data["code"],), daemon=True).start()
|
|
188
|
+
|
|
189
|
+
def _load_stats(self, code):
|
|
190
|
+
try:
|
|
191
|
+
data = api_stats(code)
|
|
192
|
+
clicks = data["clicks"]
|
|
193
|
+
date = data["created_at"][:10]
|
|
194
|
+
self.root.after(0, self.stats_var.set,
|
|
195
|
+
f"👆 {clicks} clic{'s' if clicks != 1 else ''} • 📅 Créé le {date}")
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
def _copy(self):
|
|
200
|
+
url = self.result_var.get()
|
|
201
|
+
if not url:
|
|
202
|
+
self._set_status("Aucune URL à copier.", error=True)
|
|
203
|
+
return
|
|
204
|
+
self.root.clipboard_clear()
|
|
205
|
+
self.root.clipboard_append(url)
|
|
206
|
+
self._btn_copy.config(text="✅ Copié !", fg=GREEN)
|
|
207
|
+
self.root.after(2000, lambda: self._btn_copy.config(
|
|
208
|
+
text="📋 Copier dans le presse-papier", fg=MUTED))
|
|
209
|
+
|
|
210
|
+
def _set_status(self, msg, error=False):
|
|
211
|
+
self.status_var.set(msg)
|
|
212
|
+
self.status_label.config(fg=RED_FG if error else GREEN)
|
|
213
|
+
|
|
214
|
+
def _open_settings(self):
|
|
215
|
+
win = tk.Toplevel(self.root)
|
|
216
|
+
win.title("Paramètres")
|
|
217
|
+
win.configure(bg=BG)
|
|
218
|
+
win.resizable(False, False)
|
|
219
|
+
win.geometry("360x180")
|
|
220
|
+
win.grab_set()
|
|
221
|
+
|
|
222
|
+
tk.Label(win, text="URL de l'API backend :", bg=BG, fg=MUTED,
|
|
223
|
+
font=("Segoe UI", 9, "bold")).pack(anchor="w", padx=20, pady=(20, 6))
|
|
224
|
+
|
|
225
|
+
api_var = tk.StringVar(value=get_api_url())
|
|
226
|
+
entry = tk.Entry(win, textvariable=api_var, bg=CARD, fg=TEXT,
|
|
227
|
+
insertbackground=TEXT, font=("Segoe UI", 11),
|
|
228
|
+
relief="flat", bd=0)
|
|
229
|
+
frame = tk.Frame(win, bg=BORDER, padx=1, pady=1)
|
|
230
|
+
frame.pack(fill="x", padx=20, pady=(0, 16))
|
|
231
|
+
entry.pack(fill="x", padx=10, pady=8, in_=frame)
|
|
232
|
+
|
|
233
|
+
def save():
|
|
234
|
+
url = api_var.get().strip().rstrip("/")
|
|
235
|
+
if url:
|
|
236
|
+
save_api_url(url)
|
|
237
|
+
self.api_label_var.set(f"API : {url}")
|
|
238
|
+
win.destroy()
|
|
239
|
+
|
|
240
|
+
self._make_button(win, "💾 Enregistrer", save,
|
|
241
|
+
bg=BLUE, fg="white", hover_bg=BLUE_H,
|
|
242
|
+
font_size=11, bold=True).pack(fill="x", padx=20)
|
|
243
|
+
|
|
244
|
+
# ── Helper bouton ─────────────────────────────────────────────────────────
|
|
245
|
+
def _make_button(self, parent, text, command,
|
|
246
|
+
bg=CARD, fg=TEXT, hover_bg=BORDER,
|
|
247
|
+
font_size=10, bold=False):
|
|
248
|
+
btn = tk.Button(
|
|
249
|
+
parent, text=text, command=command,
|
|
250
|
+
bg=bg, fg=fg, activebackground=hover_bg, activeforeground=fg,
|
|
251
|
+
font=("Segoe UI", font_size, "bold" if bold else "normal"),
|
|
252
|
+
relief="flat", bd=0, cursor="hand2", pady=10,
|
|
253
|
+
)
|
|
254
|
+
btn.bind("<Enter>", lambda e: btn.config(bg=hover_bg))
|
|
255
|
+
btn.bind("<Leave>", lambda e: btn.config(bg=bg))
|
|
256
|
+
return btn
|
|
257
|
+
|
|
258
|
+
def run(self):
|
|
259
|
+
self.root.mainloop()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def launch_gui():
|
|
263
|
+
app = PyShortGUI()
|
|
264
|
+
app.run()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if __name__ == "__main__":
|
|
268
|
+
launch_gui()
|