inicjalizacja projektu dodanie nowych stylow itp
This commit is contained in:
commit
e587c7a59c
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Author:Mateusz Kędziora https://mkedziora.pl
|
||||||
|
|
||||||
|
FROM golang:1.23.2-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum* ./
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY *.go ./
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
COPY --from=builder /app/main .
|
||||||
|
|
||||||
|
COPY public/ ./public
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["./main"]
|
175
main.go
Normal file
175
main.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Author:Mateusz Kędziora https://mkedziora.pl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataEntry struct {
|
||||||
|
Value string
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dataMap map[string]DataEntry
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dataMap = make(map[string]DataEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input struct {
|
||||||
|
Data string `json:"value"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Expire string `json:"expire,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
if input.ID == "" {
|
||||||
|
input.ID = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
expireString, err := strconv.Atoi(input.Expire)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expireSeconds := 30
|
||||||
|
if expireString > 0 {
|
||||||
|
if expireString > 86400 {
|
||||||
|
http.Error(w, fmt.Sprintf("Maksymalny czas wygaśnięcia to %d sekund", 86400), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expireSeconds = expireString
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationTime := time.Now().Add(time.Duration(expireSeconds) * time.Second)
|
||||||
|
|
||||||
|
dataMap[input.ID] = DataEntry{
|
||||||
|
Value: input.Data,
|
||||||
|
Timestamp: expirationTime,
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.URL.Query().Get("id")
|
||||||
|
if id == "" {
|
||||||
|
id = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
entry, exists := dataMap[id]
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if !exists || time.Since(entry.Timestamp) > 30*time.Second {
|
||||||
|
delete(dataMap, id)
|
||||||
|
http.Error(w, "No data available", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"data": entry.Value})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUrlHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.URL.Query().Get("id")
|
||||||
|
if id == "" {
|
||||||
|
id = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
entry, exists := dataMap[id]
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if !exists || time.Since(entry.Timestamp) > 30*time.Second {
|
||||||
|
delete(dataMap, id)
|
||||||
|
http.Error(w, "No data available", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||||
|
http.Redirect(w, r, entry.Value, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Utworzenie mapy do przechowywania nagłówków
|
||||||
|
headers := make(map[string]string)
|
||||||
|
|
||||||
|
// Iteracja przez wszystkie nagłówki
|
||||||
|
for name, values := range r.Header {
|
||||||
|
// Używamy wartości 0, ponieważ nagłówki mogą mieć wiele wartości
|
||||||
|
headers[name] = values[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ustawienie nagłówka Content-Type na application/json
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Zwrócenie nagłówków w formacie JSON
|
||||||
|
json.NewEncoder(w).Encode(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Obsługa endpointów API
|
||||||
|
http.HandleFunc("/api/set", setHandler)
|
||||||
|
http.HandleFunc("/api/get", getHandler)
|
||||||
|
http.HandleFunc("/api/url", getUrlHandler)
|
||||||
|
http.HandleFunc("/test", testHandler)
|
||||||
|
|
||||||
|
// Serwowanie plików statycznych
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
http.ServeFile(w, r, "public/index.html")
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, "public/app.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/style_index.css", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, "public/style_index.css")
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/style_app.css", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, "public/style_app.css")
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
for id, timestamp := range dataMap {
|
||||||
|
if time.Now().After(timestamp.Timestamp) {
|
||||||
|
delete(dataMap, id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Uruchomienie serwera
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
87
public/app.html
Normal file
87
public/app.html
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- Author:Mateusz Kędziora https://mkedziora.pl -->
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>App</title>
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<link href="/style_app.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 id="info">Click the button to start.</h1>
|
||||||
|
<a href="" id="url" rel="noopener noreferrer" target="_blank"></a>
|
||||||
|
<button id="startBtn">Start listening</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let lastData = "";
|
||||||
|
|
||||||
|
// Pobieranie parametru id z URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const id = urlParams.get("id");
|
||||||
|
let new_url = "";
|
||||||
|
let fetchingData;
|
||||||
|
let newWindow;
|
||||||
|
document
|
||||||
|
.getElementById("startBtn")
|
||||||
|
.addEventListener("click", function () {
|
||||||
|
document.getElementById("info").textContent =
|
||||||
|
"Waiting for a new address...";
|
||||||
|
newWindow = window.open("about:blank", "_blank", "");
|
||||||
|
fetchingData = setInterval(checkForNewAddress, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkForNewAddress() {
|
||||||
|
// Dodanie parametru id do URL zapytania
|
||||||
|
|
||||||
|
const apiUrl = id
|
||||||
|
? `/api/get?id=${encodeURIComponent(id)}`
|
||||||
|
: "/api/get";
|
||||||
|
|
||||||
|
fetch(apiUrl, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Pragma: "no-cache",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Brak danych");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.data && data.data !== lastData) {
|
||||||
|
lastData = data.data;
|
||||||
|
console.log("Nowy adres:", data.data);
|
||||||
|
|
||||||
|
// Przejście do nowego adresu z resetowaniem nagłówków
|
||||||
|
const url = new URL(data.data);
|
||||||
|
|
||||||
|
const a = document.getElementById("url");
|
||||||
|
a.href = url;
|
||||||
|
a.textContent = url;
|
||||||
|
console.log(newWindow);
|
||||||
|
if (newWindow) {
|
||||||
|
console.log("ustawiono nowy adres");
|
||||||
|
newWindow.location.href = id
|
||||||
|
? `/api/url?id=${encodeURIComponent(id)}`
|
||||||
|
: "/api/url";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("info").textContent =
|
||||||
|
"The page has opened; if not, click the link below. Link waiting is disabled.";
|
||||||
|
clearInterval(fetchingData);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Błąd:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprawdzaj co sekundę
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
301
public/index.html
Normal file
301
public/index.html
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- Author:Mateusz Kędziora https://mkedziora.pl -->
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5-qrcode/2.3.8/html5-qrcode.min.js"></script>
|
||||||
|
<link href="/style_index.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="startButton">Uruchom skaner QR</button>
|
||||||
|
<button id="stopButton" style="display: none">Zatrzymaj skaner</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manual-input">
|
||||||
|
<label for="idInput">ID (opcjonalne):</label>
|
||||||
|
<input type="text" id="idInput" placeholder="Wprowadź ID" />
|
||||||
|
|
||||||
|
<label for="expire">Czas wygaśnięcia (opcjonalne):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value="30"
|
||||||
|
max="86400"
|
||||||
|
id="expireInput"
|
||||||
|
placeholder="Czas wygaśnięcia"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="zoom-control" style="display: none">
|
||||||
|
<label for="zoomSlider">Powiększenie kamery:</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="zoomSlider"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
step="0.1"
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
<div class="zoom-value">1x</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="reader"></div>
|
||||||
|
|
||||||
|
<div class="or-divider">
|
||||||
|
<span class="or-text">LUB</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manual-input">
|
||||||
|
<label for="manualUrl">Wprowadź URL ręcznie:</label>
|
||||||
|
<input type="url" id="manualUrl" placeholder="https://przykład.com" />
|
||||||
|
<button id="submitUrl">Wyślij URL</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentStream = null;
|
||||||
|
let html5QrcodeScanner = null;
|
||||||
|
const statusDiv = document.getElementById("status");
|
||||||
|
const startButton = document.getElementById("startButton");
|
||||||
|
const stopButton = document.getElementById("stopButton");
|
||||||
|
const submitUrlButton = document.getElementById("submitUrl");
|
||||||
|
const manualUrlInput = document.getElementById("manualUrl");
|
||||||
|
|
||||||
|
// Funkcja do obsługi i wyświetlania błędów
|
||||||
|
function handleError(error) {
|
||||||
|
console.error("Wystąpił błąd:", error);
|
||||||
|
|
||||||
|
let errorMessage = "Wystąpił nieoczekiwany błąd.";
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
switch (error.name) {
|
||||||
|
case "NotAllowedError":
|
||||||
|
errorMessage =
|
||||||
|
"Dostęp do kamery został zabroniony. Sprawdź ustawienia przeglądarki.";
|
||||||
|
break;
|
||||||
|
case "NotFoundError":
|
||||||
|
errorMessage =
|
||||||
|
"Nie znaleziono kamery. Sprawdź czy urządzenie ma kamerę.";
|
||||||
|
break;
|
||||||
|
case "NotReadableError":
|
||||||
|
errorMessage =
|
||||||
|
"Kamera jest obecnie używana przez inną aplikację.";
|
||||||
|
break;
|
||||||
|
case "OverconstrainedError":
|
||||||
|
errorMessage =
|
||||||
|
"Nie znaleziono odpowiedniej kamery. Spróbuj ponownie.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage =
|
||||||
|
error.message ||
|
||||||
|
"Wystąpił problem z kamerą. Spróbuj odświeżyć stronę.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusDiv.textContent = errorMessage;
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendUrl(url) {
|
||||||
|
try {
|
||||||
|
statusDiv.textContent = "Wysyłanie danych...";
|
||||||
|
|
||||||
|
const id = document.getElementById("idInput").value.trim();
|
||||||
|
const expire = document.getElementById("expireInput").value.trim();
|
||||||
|
const body = { value: url };
|
||||||
|
if (id) body.id = id;
|
||||||
|
if (expire) body.expire = expire;
|
||||||
|
|
||||||
|
const response = await fetch("/api/set", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
statusDiv.textContent = "Sukces! URL został przesłany.";
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error("Błąd podczas wysyłania danych na serwer");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopScanner() {
|
||||||
|
try {
|
||||||
|
if (html5QrcodeScanner) {
|
||||||
|
await html5QrcodeScanner.stop();
|
||||||
|
html5QrcodeScanner.clear();
|
||||||
|
html5QrcodeScanner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStream) {
|
||||||
|
const tracks = currentStream.getTracks();
|
||||||
|
tracks.forEach((track) => {
|
||||||
|
if (track.readyState === "live") {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector(".zoom-control").style.display = "none";
|
||||||
|
startButton.style.display = "block";
|
||||||
|
stopButton.style.display = "none";
|
||||||
|
|
||||||
|
const readerElement = document.getElementById("reader");
|
||||||
|
if (readerElement) {
|
||||||
|
readerElement.innerHTML = "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startButton.addEventListener("click", async function () {
|
||||||
|
const zoomControl = document.querySelector(".zoom-control");
|
||||||
|
const zoomSlider = document.getElementById("zoomSlider");
|
||||||
|
const zoomValue = document.querySelector(".zoom-value");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Najpierw zatrzymaj poprzedni skaner
|
||||||
|
await stopScanner();
|
||||||
|
|
||||||
|
// Poczekaj chwilę przed ponownym uruchomieniem
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Sprawdź dostępne urządzenia
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(
|
||||||
|
(device) => device.kind === "videoinput"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (videoDevices.length === 0) {
|
||||||
|
throw new Error("Nie znaleziono żadnej kamery w urządzeniu");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferuj tylną kamerę
|
||||||
|
const camera =
|
||||||
|
videoDevices.find(
|
||||||
|
(device) =>
|
||||||
|
device.label.toLowerCase().includes("back") ||
|
||||||
|
device.label.toLowerCase().includes("tylna")
|
||||||
|
) || videoDevices[videoDevices.length - 1];
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
deviceId: camera.deviceId,
|
||||||
|
facingMode: { ideal: "environment" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
currentStream = stream;
|
||||||
|
const track = stream.getVideoTracks()[0];
|
||||||
|
|
||||||
|
if (!track) {
|
||||||
|
throw new Error("Nie udało się uzyskać dostępu do kamery");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprawdź możliwości kamery
|
||||||
|
const capabilities = track.getCapabilities();
|
||||||
|
if (capabilities && capabilities.zoom) {
|
||||||
|
zoomControl.style.display = "block";
|
||||||
|
zoomSlider.min = capabilities.zoom.min;
|
||||||
|
zoomSlider.max = capabilities.zoom.max;
|
||||||
|
zoomSlider.step =
|
||||||
|
(capabilities.zoom.max - capabilities.zoom.min) / 100;
|
||||||
|
zoomSlider.value = capabilities.zoom.min;
|
||||||
|
zoomValue.textContent = `${capabilities.zoom.min}x`;
|
||||||
|
|
||||||
|
zoomSlider.addEventListener("input", async (e) => {
|
||||||
|
try {
|
||||||
|
const zoomValue = parseFloat(e.target.value);
|
||||||
|
await track.applyConstraints({
|
||||||
|
advanced: [{ zoom: zoomValue }],
|
||||||
|
});
|
||||||
|
document.querySelector(
|
||||||
|
".zoom-value"
|
||||||
|
).textContent = `${zoomValue.toFixed(1)}x`;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Nie udało się ustawić zoomu:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
zoomControl.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
html5QrcodeScanner = new Html5Qrcode("reader");
|
||||||
|
const qrConfig = {
|
||||||
|
fps: 10,
|
||||||
|
qrbox: { width: 250, height: 250 },
|
||||||
|
aspectRatio: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
startButton.style.display = "none";
|
||||||
|
stopButton.style.display = "block";
|
||||||
|
statusDiv.textContent = "Skaner jest aktywny...";
|
||||||
|
|
||||||
|
await html5QrcodeScanner.start(
|
||||||
|
{ facingMode: "environment" },
|
||||||
|
qrConfig,
|
||||||
|
async (decodedText) => {
|
||||||
|
await stopScanner();
|
||||||
|
if (await sendUrl(decodedText)) {
|
||||||
|
manualUrlInput.value = decodedText;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Ignorujemy błędy skanowania, bo pojawiają się często gdy nie ma kodu QR w kadrze
|
||||||
|
console.debug("Skanowanie:", error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
await stopScanner();
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopButton.addEventListener("click", stopScanner);
|
||||||
|
window.addEventListener("beforeunload", stopScanner);
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.hidden && html5QrcodeScanner) {
|
||||||
|
stopScanner();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
submitUrlButton.addEventListener("click", async () => {
|
||||||
|
const url = manualUrlInput.value.trim();
|
||||||
|
if (!url) {
|
||||||
|
statusDiv.textContent = "Proszę wprowadzić URL";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(url); // Sprawdź poprawność URL
|
||||||
|
await sendUrl(url);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
statusDiv.textContent = "Wprowadzony URL jest nieprawidłowy";
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
manualUrlInput.addEventListener("keypress", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
submitUrlButton.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
121
public/style_app.css
Normal file
121
public/style_app.css
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #3498db;
|
||||||
|
--primary-hover: #2980b9;
|
||||||
|
--text-color: #2c3e50;
|
||||||
|
--background-color: #f5f7fa;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--background-color) 0%, #c3cfe2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 15px var(--shadow-color);
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#startBtn {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
#startBtn:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px var(--shadow-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#startBtn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#url {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
#url:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animacje */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dostosowania dla małych ekranów */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startBtn {
|
||||||
|
padding: 0.7rem 1.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ciemny motyw */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
}
|
||||||
|
}
|
241
public/style_index.css
Normal file
241
public/style_index.css
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #3498db;
|
||||||
|
--primary-hover: #2980b9;
|
||||||
|
--text-color: #2c3e50;
|
||||||
|
--background-color: #f5f7fa;
|
||||||
|
--border-color: #ddd;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--background-color) 0%, #c3cfe2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 15px var(--shadow-color);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px var(--shadow-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-input {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="url"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoomSlider {
|
||||||
|
flex: 1;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoomSlider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reader {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.or-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.or-divider::before,
|
||||||
|
.or-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.or-text {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dostosowania dla iOS */
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
input {
|
||||||
|
font-size: 16px; /* Zapobiega powiększaniu na iOS */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ciemny motyw */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--border-color: #333;
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="url"] {
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-control,
|
||||||
|
#status {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animacje */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dostosowania dla małych ekranów */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.7rem 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="url"] {
|
||||||
|
padding: 0.7rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
38
readme.md
Normal file
38
readme.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Fast Links - Quickly Open Links on Multiple Devices
|
||||||
|
|
||||||
|
## Project Goal
|
||||||
|
|
||||||
|
Fast Links was created to streamline teamwork and increase productivity when working with multiple devices. The application allows for instant sharing of links in real-time by creating virtual rooms where all participants see the same links.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Online Meetings**: Quickly share materials, presentations, and documents.
|
||||||
|
- **Teamwork**: Jointly browse websites, codes, and projects.
|
||||||
|
- **Education**: Use for interactive presentations and collaborative problem-solving.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Creating Virtual Rooms**: Each user can create their own room and invite others.
|
||||||
|
- **Adding Links**: Users add links that are immediately displayed on the screens of all room participants.
|
||||||
|
- **Scanning QR Codes**: Add links by scanning a QR code.
|
||||||
|
- **Scalability**: The application can handle any number of users and rooms.
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
1. **Installation**: Only Docker is required.
|
||||||
|
2. **Running**: Execute the `run_container.sh` script, optionally specifying the port using the `-p` parameter.
|
||||||
|
3. **Access**: Open the specified address in your browser.
|
||||||
|
4. **Adding Links**: Enter the link in the input field and confirm.
|
||||||
|
5. **Listening Mode**: Go to the `/app` page or specify a specific room ID using the `?id=` parameter.
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
- **Golang**: Efficient backend programming language.
|
||||||
|
- **html5-qrcode**: Library for scanning QR codes in the browser.
|
||||||
|
|
||||||
|
## Additional Suggestions
|
||||||
|
|
||||||
|
- **Documentation**: Consider creating a more detailed guide to explain advanced configuration and usage options.
|
||||||
|
- **User Interface**: Improving the UI could make the application more user-friendly.
|
||||||
|
- **Testing**: Conduct comprehensive tests to ensure application stability and reliability.
|
||||||
|
- **Security**: Ensure the security of the application, especially if it will be used to share sensitive data.
|
50
run_container.sh
Executable file
50
run_container.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Author:Mateusz Kędziora https://mkedziora.pl
|
||||||
|
|
||||||
|
# Nazwa projektu
|
||||||
|
PROJECT_NAME="fast-links"
|
||||||
|
|
||||||
|
# Domyślny port
|
||||||
|
DEFAULT_PORT=8080
|
||||||
|
|
||||||
|
# Funkcja wyświetlająca sposób użycia
|
||||||
|
usage() {
|
||||||
|
echo "Użycie: $0 [-p port]"
|
||||||
|
echo " -p port Port na którym ma działać aplikacja (domyślnie: $DEFAULT_PORT)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Przetwarzanie argumentów
|
||||||
|
while getopts ":p:" opt; do
|
||||||
|
case ${opt} in
|
||||||
|
p )
|
||||||
|
PORT=$OPTARG
|
||||||
|
;;
|
||||||
|
\? )
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Jeśli port nie został podany, użyj domyślnego
|
||||||
|
PORT=${PORT:-$DEFAULT_PORT}
|
||||||
|
|
||||||
|
# Zatrzymaj i usuń stary kontener, jeśli istnieje
|
||||||
|
echo "Zatrzymywanie i usuwanie starego kontenera..."
|
||||||
|
docker stop $PROJECT_NAME 2>/dev/null
|
||||||
|
docker rm $PROJECT_NAME 2>/dev/null
|
||||||
|
|
||||||
|
# Zbuduj nowy obraz
|
||||||
|
echo "Budowanie nowego obrazu..."
|
||||||
|
docker build -t $PROJECT_NAME .
|
||||||
|
|
||||||
|
# Uruchom nowy kontener
|
||||||
|
echo "Uruchamianie nowego kontenera na porcie $PORT..."
|
||||||
|
docker run -d --name $PROJECT_NAME -p $PORT:8080 $PROJECT_NAME
|
||||||
|
|
||||||
|
# Wyświetl informacje o uruchomionym kontenerze
|
||||||
|
echo "Kontener uruchomiony. Szczegóły:"
|
||||||
|
docker ps --filter name=$PROJECT_NAME
|
||||||
|
|
||||||
|
echo "Aplikacja dostępna pod adresem: http://localhost:$PORT"
|
Loading…
Reference in New Issue
Block a user