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