mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-07-13 16:16:44 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e424b44cb2 |
@@ -24,7 +24,6 @@ import kst4contest.logic.PriorityCalculator;
|
|||||||
import kst4contest.model.*;
|
import kst4contest.model.*;
|
||||||
import kst4contest.test.MockKstServer;
|
import kst4contest.test.MockKstServer;
|
||||||
import kst4contest.utils.PlayAudioUtils;
|
import kst4contest.utils.PlayAudioUtils;
|
||||||
import kst4contest.locatorUtils.Location;
|
|
||||||
import kst4contest.view.Kst4ContestApplication;
|
import kst4contest.view.Kst4ContestApplication;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@@ -35,7 +34,6 @@ import java.util.function.Predicate;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Central Chat kst4contest.controller. Instantiate only one time per category of kst Chat.
|
* Central Chat kst4contest.controller. Instantiate only one time per category of kst Chat.
|
||||||
@@ -320,8 +318,10 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
|
|
||||||
EnumSet<Band> offered = EnumSet.noneOf(Band.class);
|
EnumSet<Band> offered = EnumSet.noneOf(Band.class);
|
||||||
|
|
||||||
for (ChatMember cm : findActiveChatMembersByRawCall(callRaw)) {
|
synchronized (getLst_chatMemberList()) {
|
||||||
|
for (ChatMember cm : getLst_chatMemberList()) {
|
||||||
if (cm == null || cm.getCallSignRaw() == null) continue;
|
if (cm == null || cm.getCallSignRaw() == null) continue;
|
||||||
|
if (!normalizeCallRaw(cm.getCallSignRaw()).equals(callRaw)) continue;
|
||||||
|
|
||||||
Map<Band, ChatMember.ActiveFrequencyInfo> map = cm.getKnownActiveBands();
|
Map<Band, ChatMember.ActiveFrequencyInfo> map = cm.getKnownActiveBands();
|
||||||
if (map == null || map.isEmpty()) continue;
|
if (map == null || map.isEmpty()) continue;
|
||||||
@@ -340,9 +340,11 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
+ " freq=" + e.getValue().frequency
|
+ " freq=" + e.getValue().frequency
|
||||||
+ " ageMs=" + age);
|
+ " ageMs=" + age);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return offered;
|
return offered;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,8 +356,10 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
|
|
||||||
EnumSet<Band> worked = EnumSet.noneOf(Band.class);
|
EnumSet<Band> worked = EnumSet.noneOf(Band.class);
|
||||||
|
|
||||||
for (ChatMember cm : findActiveChatMembersByRawCall(callRaw)) {
|
synchronized (getLst_chatMemberList()) {
|
||||||
|
for (ChatMember cm : getLst_chatMemberList()) {
|
||||||
if (cm == null || cm.getCallSignRaw() == null) continue;
|
if (cm == null || cm.getCallSignRaw() == null) continue;
|
||||||
|
if (!normalizeCallRaw(cm.getCallSignRaw()).equals(callRaw)) continue;
|
||||||
|
|
||||||
if (cm.isWorked144()) worked.add(Band.B_144);
|
if (cm.isWorked144()) worked.add(Band.B_144);
|
||||||
if (cm.isWorked432()) worked.add(Band.B_432);
|
if (cm.isWorked432()) worked.add(Band.B_432);
|
||||||
@@ -364,9 +368,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
if (cm.isWorked3400()) worked.add(Band.B_3400);
|
if (cm.isWorked3400()) worked.add(Band.B_3400);
|
||||||
if (cm.isWorked5600()) worked.add(Band.B_5760);
|
if (cm.isWorked5600()) worked.add(Band.B_5760);
|
||||||
if (cm.isWorked10G()) worked.add(Band.B_10G);
|
if (cm.isWorked10G()) worked.add(Band.B_10G);
|
||||||
if (cm.isWorked24G()) worked.add(Band.B_24G);
|
if (cm.isWorked24G()) worked.add(Band.B_24G); // optional, only if your Band enum supports it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return worked;
|
return worked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,8 +645,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
|
|
||||||
// rotatorClient.
|
// rotatorClient.
|
||||||
|
|
||||||
// this.lst_chatMemberList.clear();;
|
this.lst_chatMemberList.clear();;
|
||||||
this.clearActiveChatMembers();
|
|
||||||
this.lst_clusterMemberList.clear();
|
this.lst_clusterMemberList.clear();
|
||||||
|
|
||||||
this.setDisconnected(true);
|
this.setDisconnected(true);
|
||||||
@@ -710,7 +713,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
}
|
}
|
||||||
} else if (action.equals(ApplicationConstants.DISCSTRING_DISCONNECTONLY)){
|
} else if (action.equals(ApplicationConstants.DISCSTRING_DISCONNECTONLY)){
|
||||||
|
|
||||||
this.clearActiveChatMembers();
|
this.lst_chatMemberList.clear();;
|
||||||
this.lst_clusterMemberList.clear();
|
this.lst_clusterMemberList.clear();
|
||||||
|
|
||||||
|
|
||||||
@@ -956,9 +959,16 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
||||||
List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
synchronized (getLst_chatMemberList()) {
|
||||||
return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
for (ChatMember member : getLst_chatMemberList()) {
|
||||||
|
if (member == null || member.getCallSignRaw() == null) continue;
|
||||||
|
if (normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
||||||
@@ -966,43 +976,22 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
||||||
|
synchronized (getLst_chatMemberList()) {
|
||||||
|
for (ChatMember member : getLst_chatMemberList()) {
|
||||||
|
if (member == null || member.getCallSignRaw() == null) continue;
|
||||||
|
if (!normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) continue;
|
||||||
|
|
||||||
String locator = member.getQra();
|
String locator = member.getQra();
|
||||||
if (locator != null && !locator.isBlank()) {
|
if (locator != null && !locator.isBlank()) {
|
||||||
return locator.trim().toUpperCase(Locale.ROOT);
|
return locator.trim().toUpperCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
|
||||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
|
||||||
// return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
|
||||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
|
||||||
//
|
|
||||||
// for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
|
||||||
// String locator = member.getQra();
|
|
||||||
// if (locator != null && !locator.isBlank()) {
|
|
||||||
// return locator.trim().toUpperCase(Locale.ROOT);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
public StationMetricsService getStationMetricsService() {
|
public StationMetricsService getStationMetricsService() {
|
||||||
return stationMetricsService;
|
return stationMetricsService;
|
||||||
}
|
}
|
||||||
@@ -1025,14 +1014,10 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
return new HashMap<>(lastInboundCategoryByCallSignRaw);
|
return new HashMap<>(lastInboundCategoryByCallSignRaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// public List<ChatMember> snapshotChatMembers() {
|
|
||||||
// synchronized (getLst_chatMemberList()) {
|
|
||||||
// return new ArrayList<>(getLst_chatMemberList());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
public List<ChatMember> snapshotChatMembers() {
|
public List<ChatMember> snapshotChatMembers() {
|
||||||
return new ArrayList<>(activeChatMembersByCallAndCategory.values());
|
synchronized (getLst_chatMemberList()) {
|
||||||
|
return new ArrayList<>(getLst_chatMemberList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ContestSked> snapshotActiveSkeds() {
|
public List<ContestSked> snapshotActiveSkeds() {
|
||||||
@@ -1141,27 +1126,13 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
private ObservableList<ChatMember> chatMemberList = FXCollections.observableArrayList(); // List of active stations
|
private ObservableList<ChatMember> chatMemberList = FXCollections.observableArrayList(); // List of active stations
|
||||||
|
|
||||||
|
|
||||||
|
private ObservableList<ChatMember> lst_chatMemberList = FXCollections.synchronizedObservableList(chatMemberList); // List
|
||||||
// of active stn in chat
|
// of active stn in chat
|
||||||
private FilteredList<ChatMember> lst_chatMemberListFiltered = new FilteredList<ChatMember>(chatMemberList);
|
private FilteredList<ChatMember> lst_chatMemberListFiltered = new FilteredList<ChatMember>(chatMemberList);
|
||||||
private SortedList<ChatMember> lst_chatMemberSortedFilteredList = new SortedList<ChatMember>(lst_chatMemberListFiltered);
|
private SortedList<ChatMember> lst_chatMemberSortedFilteredList = new SortedList<ChatMember>(lst_chatMemberListFiltered);
|
||||||
|
|
||||||
private ObservableList<ChatMember> lst_chatMemberList = FXCollections.synchronizedObservableList(chatMemberList); // List
|
|
||||||
private ObservableList<Predicate<ChatMember>> lst_chatMemberListFilterPredicates = FXCollections.observableArrayList();
|
private ObservableList<Predicate<ChatMember>> lst_chatMemberListFilterPredicates = FXCollections.observableArrayList();
|
||||||
private ObservableList<ClusterMessage> lst_clusterMemberList = FXCollections.observableArrayList();
|
private ObservableList<ClusterMessage> lst_clusterMemberList = FXCollections.observableArrayList();
|
||||||
|
|
||||||
/*
|
|
||||||
* Thread-safe active-member model.
|
|
||||||
*
|
|
||||||
* MessageBusManagementThread must not use the JavaFX TableView backing list as
|
|
||||||
* its primary data model. Otherwise every add/remove fires FilteredList,
|
|
||||||
* SortedList and TableView selection listeners on the MessageBus thread and JavaFX
|
|
||||||
* can throw "Not on FX application thread" or internal listener NPEs.
|
|
||||||
*
|
|
||||||
* This map is the worker-thread safe source of truth. The ObservableList below
|
|
||||||
* remains a UI mirror and is mutated only on the JavaFX application thread.
|
|
||||||
*/
|
|
||||||
private final java.util.concurrent.ConcurrentMap<String, ChatMember> activeChatMembersByCallAndCategory =
|
|
||||||
new java.util.concurrent.ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Message table update buffers.
|
* Message table update buffers.
|
||||||
@@ -1190,254 +1161,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
// ******************************************************************************************************************************************
|
// ******************************************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes UI-bound work on the JavaFX application thread.
|
|
||||||
*
|
|
||||||
* Worker threads may call this safely. If the caller is already on the FX
|
|
||||||
* thread, the action is executed immediately to preserve ordering.
|
|
||||||
*/
|
|
||||||
private void runOnFxThread(Runnable action) {
|
|
||||||
if (action == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Platform.isFxApplicationThread()) {
|
|
||||||
action.run();
|
|
||||||
} else {
|
|
||||||
Platform.runLater(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the active-member key. The raw/base callsign alone is not enough
|
|
||||||
* because the same station can be logged into multiple ON4KST categories at
|
|
||||||
* the same time. Therefore the category number is part of the key.
|
|
||||||
*/
|
|
||||||
private String buildActiveChatMemberKey(String callSign, ChatCategory category) {
|
|
||||||
String normalizedCallsign = ChatMember.normalizeCallSignToBaseCallSign(callSign);
|
|
||||||
if (normalizedCallsign == null || normalizedCallsign.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int categoryNumber = category == null ? -1 : category.getCategoryNumber();
|
|
||||||
return normalizedCallsign.trim().toUpperCase(Locale.ROOT) + "|" + categoryNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildActiveChatMemberKey(ChatMember member) {
|
|
||||||
if (member == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String callSign = member.getCallSignRaw() != null ? member.getCallSignRaw() : member.getCallSign();
|
|
||||||
return buildActiveChatMemberKey(callSign, member.getChatCategory());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds or replaces an active chat member in the worker-thread model and mirrors
|
|
||||||
* that change to the JavaFX list. This is the only supported path for ON4KST
|
|
||||||
* user-enter events.
|
|
||||||
*/
|
|
||||||
public void addOrUpdateActiveChatMember(ChatMember member) {
|
|
||||||
String key = buildActiveChatMemberKey(member);
|
|
||||||
if (key == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeChatMembersByCallAndCategory.put(key, member);
|
|
||||||
|
|
||||||
runOnFxThread(() -> {
|
|
||||||
int existingIndex = findChatMemberIndexInUiListByKey(key);
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
lst_chatMemberList.set(existingIndex, member);
|
|
||||||
} else {
|
|
||||||
lst_chatMemberList.add(member);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes an active chat member from the worker-thread model and from the UI
|
|
||||||
* mirror. Removal from the ObservableList is always performed on the FX thread
|
|
||||||
* because the list is bound to FilteredList/SortedList/TableView.
|
|
||||||
*/
|
|
||||||
public boolean removeActiveChatMember(ChatMember member) {
|
|
||||||
String key = buildActiveChatMemberKey(member);
|
|
||||||
if (key == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMember removedMember = activeChatMembersByCallAndCategory.remove(key);
|
|
||||||
runOnFxThread(() -> lst_chatMemberList.removeIf(currentMember -> key.equals(buildActiveChatMemberKey(currentMember))));
|
|
||||||
return removedMember != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the active-member model and the JavaFX UI mirror. Use this instead of
|
|
||||||
* getLst_chatMemberList().clear() from timers or worker threads.
|
|
||||||
*/
|
|
||||||
public void clearActiveChatMembers() {
|
|
||||||
activeChatMembersByCallAndCategory.clear();
|
|
||||||
runOnFxThread(() -> lst_chatMemberList.clear());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a member from the thread-safe active model. This avoids reading the
|
|
||||||
* TableView backing list from MessageBusManagementThread.
|
|
||||||
*/
|
|
||||||
public ChatMember findActiveChatMember(ChatMember lookForThis) {
|
|
||||||
String key = buildActiveChatMemberKey(lookForThis);
|
|
||||||
return key == null ? null : activeChatMembersByCallAndCategory.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatMember findActiveChatMember(String callSign, ChatCategory category) {
|
|
||||||
String key = buildActiveChatMemberKey(callSign, category);
|
|
||||||
return key == null ? null : activeChatMembersByCallAndCategory.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getActiveChatMemberCount() {
|
|
||||||
return activeChatMembersByCallAndCategory.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all active category variants of a callsign. This is used when a QRG or
|
|
||||||
* band hint should be propagated from one category instance to the same station
|
|
||||||
* in another category.
|
|
||||||
*/
|
|
||||||
public List<ChatMember> findActiveChatMembersByRawCall(String callSignRaw) {
|
|
||||||
String normalizedCallsign = ChatMember.normalizeCallSignToBaseCallSign(callSignRaw);
|
|
||||||
if (normalizedCallsign == null || normalizedCallsign.isBlank()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
String normalizedKeyPart = normalizedCallsign.trim().toUpperCase(Locale.ROOT);
|
|
||||||
List<ChatMember> matchingMembers = new ArrayList<>();
|
|
||||||
|
|
||||||
for (ChatMember member : activeChatMembersByCallAndCategory.values()) {
|
|
||||||
if (member == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String memberCall = member.getCallSignRaw() != null ? member.getCallSignRaw() : member.getCallSign();
|
|
||||||
String normalizedMemberCall = ChatMember.normalizeCallSignToBaseCallSign(memberCall);
|
|
||||||
if (normalizedMemberCall != null && normalizedMemberCall.equalsIgnoreCase(normalizedKeyPart)) {
|
|
||||||
matchingMembers.add(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchingMembers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates locator and derived direction values in the active model. The UI list
|
|
||||||
* contains the same member object, but the user-list refresh is still triggered
|
|
||||||
* on the JavaFX thread so TableView/map derived displays can repaint safely.
|
|
||||||
*/
|
|
||||||
public boolean updateActiveChatMemberLocator(ChatMember lookupMember, String newLocator) {
|
|
||||||
ChatMember activeMember = findActiveChatMember(lookupMember);
|
|
||||||
if (activeMember == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeMember.setQra(newLocator);
|
|
||||||
activeMember.setQrb(new Location().getDistanceKmByTwoLocatorStrings(chatPreferences.getStn_loginLocatorMainCat(), newLocator));
|
|
||||||
activeMember.setQTFdirection(new Location(chatPreferences.getStn_loginLocatorMainCat()).getBearing(new Location(newLocator)));
|
|
||||||
fireUserListUpdate("Locator changed");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean updateActiveChatMemberState(ChatMember lookupMember, int newState) {
|
|
||||||
ChatMember activeMember = findActiveChatMember(lookupMember);
|
|
||||||
if (activeMember == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeMember.setState(newState);
|
|
||||||
fireUserListUpdate("User state changed");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a UM3-style user-info update only to an already active chat member.
|
|
||||||
*
|
|
||||||
* ON4KST may send UM3 messages for stations that are not logged into the
|
|
||||||
* current channel, for example when a user changes locator/profile data on the
|
|
||||||
* website. Such stations must not be added to the visible chat member list,
|
|
||||||
* because they cannot be addressed in the channel. Their full information will
|
|
||||||
* be delivered again with UA0/UA5/UA2 when they actually join.
|
|
||||||
*
|
|
||||||
* @return true if an active member was found and updated, false if the UM3
|
|
||||||
* information was intentionally ignored.
|
|
||||||
*/
|
|
||||||
public boolean updateActiveChatMemberInfoIfPresent(ChatMember updatedMember) {
|
|
||||||
String key = buildActiveChatMemberKey(updatedMember);
|
|
||||||
if (key == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMember activeMember = activeChatMembersByCallAndCategory.get(key);
|
|
||||||
if (activeMember == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeMember.setName(updatedMember.getName());
|
|
||||||
activeMember.setQra(updatedMember.getQra());
|
|
||||||
activeMember.setState(updatedMember.getState());
|
|
||||||
activeMember.setLastActivity(updatedMember.getLastActivity());
|
|
||||||
activeMember.setActivityTimeLastInEpoch(updatedMember.getActivityTimeLastInEpoch());
|
|
||||||
activeMember.setQrb(updatedMember.getQrb());
|
|
||||||
activeMember.setQTFdirection(updatedMember.getQTFdirection());
|
|
||||||
fireUserListUpdate("User info updated");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backward-compatible wrapper for older call sites. Despite the historic name,
|
|
||||||
* this method no longer adds unknown UM3 users to the active member model.
|
|
||||||
*/
|
|
||||||
public boolean updateOrAddActiveChatMemberInfo(ChatMember updatedMember) {
|
|
||||||
return updateActiveChatMemberInfoIfPresent(updatedMember);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores a newly detected QRG/band on all active category variants of a station.
|
|
||||||
* The old StringProperty is still set for compatibility with existing TableView
|
|
||||||
* columns and DXCluster code.
|
|
||||||
*/
|
|
||||||
public void applyDetectedFrequencyToActiveMembers(ChatMember sender, Band detectedBand, double detectedFrequencyMhz) {
|
|
||||||
if (sender == null || detectedBand == null || detectedFrequencyMhz <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String displayFrequency = String.valueOf(detectedFrequencyMhz);
|
|
||||||
String rawCall = sender.getCallSignRaw() != null ? sender.getCallSignRaw() : sender.getCallSign();
|
|
||||||
|
|
||||||
for (ChatMember member : findActiveChatMembersByRawCall(rawCall)) {
|
|
||||||
if (member == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
member.addKnownFrequency(detectedBand, detectedFrequencyMhz);
|
|
||||||
member.setFrequency(new SimpleStringProperty(displayFrequency));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also cover dummy senders that are not in the active model, e.g. [n/a] senders.
|
|
||||||
sender.addKnownFrequency(detectedBand, detectedFrequencyMhz);
|
|
||||||
sender.setFrequency(new SimpleStringProperty(displayFrequency));
|
|
||||||
fireUserListUpdate("Frequency detected");
|
|
||||||
}
|
|
||||||
|
|
||||||
private int findChatMemberIndexInUiListByKey(String key) {
|
|
||||||
if (key == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < lst_chatMemberList.size(); i++) {
|
|
||||||
if (key.equals(buildActiveChatMemberKey(lst_chatMemberList.get(i)))) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2288,7 +2011,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
socket.close();
|
socket.close();
|
||||||
|
|
||||||
chatController.setConnectedAndLoggedIn(false);
|
chatController.setConnectedAndLoggedIn(false);
|
||||||
chatController.clearActiveChatMembers();
|
chatController.getLst_chatMemberList().clear();
|
||||||
|
|
||||||
System.out.println("[Chatcontroller, Warning: ] Socket closed or disconnected");
|
System.out.println("[Chatcontroller, Warning: ] Socket closed or disconnected");
|
||||||
|
|
||||||
|
|||||||
@@ -324,12 +324,31 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
// --- STEP 3: Process Result ---
|
// --- STEP 3: Process Result ---
|
||||||
if (finalDetectedBand != null && finalDetectedFrequency > 0) {
|
if (finalDetectedBand != null && finalDetectedFrequency > 0) {
|
||||||
|
|
||||||
/*
|
// 1. Store in the new Map (for future context/history)
|
||||||
* Store the detected QRG in the thread-safe active-member model.
|
|
||||||
* The UI table is only a JavaFX mirror, so the MessageBus must not scan
|
sender.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
|
||||||
* or mutate getLst_chatMemberList() here.
|
// No automatic full terrain analysis here.
|
||||||
*/
|
// The frequency is stored for later map/manual reachability requests.
|
||||||
client.applyDetectedFrequencyToActiveMembers(sender, finalDetectedBand, finalDetectedFrequency);
|
|
||||||
|
//propagate known frequency to all instances of the same callsign (callRaw may exist multiple times)
|
||||||
|
try {
|
||||||
|
ArrayList<Integer> sameCallIdx = client.checkListForChatMemberIndexesByCallSign(sender);
|
||||||
|
for (int idx : sameCallIdx) {
|
||||||
|
ChatMember cm = client.getLst_chatMemberList().get(idx);
|
||||||
|
if (cm != null && cm != sender) {
|
||||||
|
cm.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
|
||||||
|
// No automatic full terrain analysis here.
|
||||||
|
// Avoids exhausting the online terrain API when many stations mention QRGs.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("[SmartParser, warning]: failed to propagate known frequency across duplicates: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 2. Set the old String-Property for GUI compatibility
|
||||||
|
// We assume standard display format (MHz)
|
||||||
|
sender.setFrequency(new javafx.beans.property.SimpleStringProperty(String.valueOf(finalDetectedFrequency)));
|
||||||
|
|
||||||
System.out.println("[SmartParser] Detected for " + sender.getCallSign() + ": " +
|
System.out.println("[SmartParser] Detected for " + sender.getCallSign() + ": " +
|
||||||
finalDetectedFrequency + " MHz (" + finalDetectedBand + ") " +
|
finalDetectedFrequency + " MHz (" + finalDetectedBand + ") " +
|
||||||
@@ -472,92 +491,6 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
return -1; // if it´s not found, the method will always end here and return -1
|
return -1; // if it´s not found, the method will always end here and return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a minimal receiver object for ON4KST broadcast messages.
|
|
||||||
* Keeping this as an object, not null, protects all downstream chat filters
|
|
||||||
* and debug logging from null receiver paths.
|
|
||||||
*/
|
|
||||||
private ChatMember createAllReceiver() {
|
|
||||||
ChatMember dummy = new ChatMember();
|
|
||||||
dummy.setCallSign("ALL");
|
|
||||||
dummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
|
|
||||||
return dummy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a message sender from the thread-safe active-member model. If a
|
|
||||||
* CH/CR message arrives before the matching user-enter message, a marked
|
|
||||||
* fallback sender is used. The returned sender is never null.
|
|
||||||
*/
|
|
||||||
private ChatMember resolveInboundSender(String senderCallSign, ChatCategory category, ChatMessage message) {
|
|
||||||
ChatMember lookup = new ChatMember();
|
|
||||||
lookup.setCallSign(senderCallSign);
|
|
||||||
lookup.setChatCategory(category);
|
|
||||||
|
|
||||||
ChatMember senderObj = this.client.findActiveChatMember(lookup);
|
|
||||||
if (senderObj != null) {
|
|
||||||
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
|
|
||||||
|
|
||||||
// Remember the last active category so later outgoing replies can be routed correctly.
|
|
||||||
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
|
|
||||||
|
|
||||||
// Metrics influence priority scoring; process them after message text is known.
|
|
||||||
this.client.getStationMetricsService().onInboundMessage(
|
|
||||||
senderObj.getCallSignRaw(),
|
|
||||||
System.currentTimeMillis(),
|
|
||||||
message == null ? null : message.getMessageText(),
|
|
||||||
this.client.getChatPreferences(),
|
|
||||||
this.client.getChatPreferences().getStn_loginCallSign()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.client.getScoreService().requestRecompute("rx-chat-message");
|
|
||||||
return senderObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMember fallbackSender = new ChatMember();
|
|
||||||
String myCall = this.client.getChatPreferences().getStn_loginCallSign();
|
|
||||||
if (senderCallSign != null && senderCallSign.equalsIgnoreCase(myCall)) {
|
|
||||||
fallbackSender.setCallSign(myCall);
|
|
||||||
} else {
|
|
||||||
fallbackSender.setCallSign("[n/a]" + senderCallSign);
|
|
||||||
}
|
|
||||||
fallbackSender.setChatCategory(category);
|
|
||||||
fallbackSender.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
|
|
||||||
return fallbackSender;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a message receiver from the active-member model. Unknown receivers
|
|
||||||
* are represented as explicit fallback objects instead of null. This keeps PM
|
|
||||||
* echo display and historic messages stable even if the target station already
|
|
||||||
* left the chat.
|
|
||||||
*/
|
|
||||||
private ChatMember resolveInboundReceiver(String receiverCallSign, ChatCategory category) {
|
|
||||||
if (receiverCallSign == null || receiverCallSign.equals("0")) {
|
|
||||||
return createAllReceiver();
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMember lookup = new ChatMember();
|
|
||||||
lookup.setCallSign(receiverCallSign);
|
|
||||||
lookup.setChatCategory(category);
|
|
||||||
|
|
||||||
ChatMember receiverObj = this.client.findActiveChatMember(lookup);
|
|
||||||
if (receiverObj != null) {
|
|
||||||
return receiverObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMember fallbackReceiver = new ChatMember();
|
|
||||||
String myCall = this.client.getChatPreferences().getStn_loginCallSign();
|
|
||||||
if (receiverCallSign.equalsIgnoreCase(myCall)) {
|
|
||||||
fallbackReceiver.setCallSign(myCall);
|
|
||||||
} else {
|
|
||||||
fallbackReceiver.setCallSign(receiverCallSign + "(left)");
|
|
||||||
}
|
|
||||||
fallbackReceiver.setChatCategory(category);
|
|
||||||
fallbackReceiver.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
|
|
||||||
return fallbackReceiver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes received messages via port 23001 (improved telnet Interface)
|
* Processes received messages via port 23001 (improved telnet Interface)
|
||||||
*
|
*
|
||||||
@@ -657,7 +590,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
|
|
||||||
if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) {
|
if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) {
|
||||||
this.client.addOrUpdateActiveChatMember(newMember); // the own call will not be in the list
|
this.client.getLst_chatMemberList().add(newMember); //the own call will not be in the list
|
||||||
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
|
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
|
||||||
// Reachability is calculated on demand only: map click, selected station, or manual request.
|
// Reachability is calculated on demand only: map click, selected station, or manual request.
|
||||||
}
|
}
|
||||||
@@ -704,7 +637,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
newMember = this.client.getDbHandler().fetchChatMemberWkdDataForOnlyOneCallsignFromDB(newMember);
|
newMember = this.client.getDbHandler().fetchChatMemberWkdDataForOnlyOneCallsignFromDB(newMember);
|
||||||
|
|
||||||
this.client.addOrUpdateActiveChatMember(newMember);
|
this.client.getLst_chatMemberList().add(newMember);
|
||||||
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
|
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
|
||||||
|
|
||||||
this.client.getDbHandler().storeChatMember(newMember);
|
this.client.getDbHandler().storeChatMember(newMember);
|
||||||
@@ -732,11 +665,17 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
newMember.setCallSign(splittedMessageLine[2]);
|
newMember.setCallSign(splittedMessageLine[2]);
|
||||||
|
|
||||||
System.out.println("[MSGBUSMGT, Info:] User left Chat and will be removed from list ["
|
System.out.println("[MSGBUSMGT, Info:] User left Chat and will be removed from list ["
|
||||||
+ this.client.getActiveChatMemberCount() + "] :" + newMember.getCallSign());
|
+ this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign());
|
||||||
|
try {
|
||||||
|
this.client.getLst_chatMemberList().remove(
|
||||||
|
checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), newMember));
|
||||||
|
|
||||||
if (!this.client.removeActiveChatMember(newMember)) {
|
//since 1.26 new method design to detect chatcategory, too!
|
||||||
System.out.println("[MSGBUSMGT, Info:] User sent left chat but was not active: "
|
|
||||||
+ newMember.getCallSign() + " / " + newMember.getChatCategory());
|
} catch (Exception e) {
|
||||||
|
System.out.println("[MSGBUSMGT, EXC!, Error:] User sent left chat but had not been there ... ["
|
||||||
|
+ this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign() + "\n"
|
||||||
|
+ e.getStackTrace());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
} else
|
||||||
@@ -756,12 +695,9 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
|
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
|
||||||
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
|
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
|
||||||
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
|
|
||||||
newMessageArrived.setMessageText(splittedMessageLine[6]);
|
|
||||||
|
|
||||||
if (splittedMessageLine[3].equals("SERVER")) {
|
if (splittedMessageLine[3].equals("SERVER")) {
|
||||||
ChatMember dummy = new ChatMember();
|
ChatMember dummy = new ChatMember();
|
||||||
dummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
|
|
||||||
dummy.setCallSign("SERVER");
|
dummy.setCallSign("SERVER");
|
||||||
dummy.setName("Sysop");
|
dummy.setName("Sysop");
|
||||||
newMessageArrived.setSender(dummy);
|
newMessageArrived.setSender(dummy);
|
||||||
@@ -771,16 +707,77 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
newMessageArrived.setSender(resolveInboundSender(
|
ChatMember sender = new ChatMember();
|
||||||
splittedMessageLine[3],
|
sender.setCallSign(splittedMessageLine[3]);
|
||||||
chategoryForMessageAndMessageSender,
|
sender.setChatCategory(chategoryForMessageAndMessageSender);
|
||||||
newMessageArrived));
|
|
||||||
|
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), sender);
|
||||||
|
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
//user not found in the chatmember list
|
||||||
|
try {
|
||||||
|
// newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
|
||||||
|
// this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
|
||||||
|
|
||||||
|
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
|
||||||
|
newMessageArrived.setSender(senderObj);
|
||||||
|
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
|
||||||
|
|
||||||
|
// Remember last inbound category per callsignRaw (required for correct send-routing later)
|
||||||
|
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
|
||||||
|
|
||||||
|
// Metrics for scoring: momentum, response-time, no-reply, positive signals
|
||||||
|
this.client.getStationMetricsService().onInboundMessage(
|
||||||
|
senderObj.getCallSignRaw(),
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
newMessageArrived.getMessageText(),
|
||||||
|
this.client.getChatPreferences(),
|
||||||
|
this.client.getChatPreferences().getStn_loginCallSign()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Activity/category changes influence priority => request recompute
|
||||||
|
this.client.getScoreService().requestRecompute("rx-chat-message");
|
||||||
|
|
||||||
|
} catch (Exception exc) {
|
||||||
|
ChatMember aSenderDummy = new ChatMember();
|
||||||
|
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
|
||||||
|
aSenderDummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
|
||||||
|
newMessageArrived.setSender(aSenderDummy);
|
||||||
|
System.out.println("MsgBusmgtT: Catched Error! " + exc.getMessage() + " // " + splittedMessageLine[3] + " is not in the list! Faking sender!");
|
||||||
|
exc.printStackTrace();
|
||||||
|
}
|
||||||
|
// b4 init list
|
||||||
|
} else {
|
||||||
|
//user not found in chatmember list, mark it, sender can not be set
|
||||||
|
if (!sender.getCallSign().equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
|
||||||
|
sender.setCallSign("[n/a]" + sender.getCallSign());
|
||||||
|
// if someone sent a message without being in the userlist (cause
|
||||||
|
// on4kst missed implementing....), callsign will be marked
|
||||||
|
} else {
|
||||||
|
//that means, message was by own station, broadcasted to all other
|
||||||
|
ChatMember dummy = new ChatMember();
|
||||||
|
dummy.setCallSign("ALL");
|
||||||
|
newMessageArrived.setReceiver(dummy);
|
||||||
|
|
||||||
|
AirPlaneReflectionInfo preventNullpointerExc = new AirPlaneReflectionInfo();
|
||||||
|
preventNullpointerExc.setAirPlanesReachableCntr(0);
|
||||||
|
sender.setAirPlaneReflectInfo(preventNullpointerExc);
|
||||||
|
newMessageArrived.setSender(sender); //my own call is the sender
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newMessageArrived.setSender(this.client.getChatMemberTable().get(splittedMessageLine[3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
|
||||||
|
newMessageArrived.setMessageText(splittedMessageLine[6]);
|
||||||
|
|
||||||
if (splittedMessageLine[7].equals("0")) {
|
if (splittedMessageLine[7].equals("0")) {
|
||||||
// message is not directed to anyone, move it to the cq messages.
|
// message is not directed to anyone, move it to the cq messages!
|
||||||
newMessageArrived.setReceiver(createAllReceiver());
|
ChatMember dummy = new ChatMember();
|
||||||
|
dummy.setCallSign("ALL");
|
||||||
|
newMessageArrived.setReceiver(dummy);
|
||||||
|
|
||||||
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
|
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
|
||||||
|
|
||||||
@@ -789,9 +786,36 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
} else {
|
} else {
|
||||||
//message is directed to another chatmember, process as such!
|
//message is directed to another chatmember, process as such!
|
||||||
|
|
||||||
newMessageArrived.setReceiver(resolveInboundReceiver(
|
ChatMember receiver = new ChatMember();
|
||||||
splittedMessageLine[7],
|
|
||||||
chategoryForMessageAndMessageSender));
|
receiver.setChatCategory(chategoryForMessageAndMessageSender); //got out of message itself
|
||||||
|
|
||||||
|
receiver.setCallSign(splittedMessageLine[7]);
|
||||||
|
|
||||||
|
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), receiver);
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
newMessageArrived.setReceiver(this.client.getLst_chatMemberList().get(index));// -1: Member left Chat
|
||||||
|
// before...
|
||||||
|
} else { //found in active member list
|
||||||
|
|
||||||
|
|
||||||
|
if (receiver.getCallSign().equals(client.getChatPreferences().getStn_loginCallSign())) {
|
||||||
|
/**
|
||||||
|
* If mycallsign sent a message to the server, server will publish that message and
|
||||||
|
* send it to all chatmember including me.
|
||||||
|
* As mycall is not in the userlist, the message would not been displayed if I handle
|
||||||
|
* it in the next case (marking left user, just for information). But I want an echo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
receiver.setCallSign(client.getChatPreferences().getStn_loginCallSign());
|
||||||
|
newMessageArrived.setReceiver(receiver);
|
||||||
|
} else {
|
||||||
|
//this are user which left chat but had been adressed by this message
|
||||||
|
receiver.setCallSign(receiver.getCallSign() + "(left)");
|
||||||
|
newMessageArrived.setReceiver(receiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
|
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
|
||||||
|
|
||||||
@@ -955,21 +979,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
// the "to me message list" with modified messagetext, added rxers callsign
|
// the "to me message list" with modified messagetext, added rxers callsign
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
/*
|
//message sent to other user
|
||||||
* Message sent from one other station to another other station.
|
|
||||||
*
|
|
||||||
* The old code reached this point only with members from the visible user list.
|
|
||||||
* With explicit fallback sender/receiver objects, this path may also see
|
|
||||||
* stations that already left the chat or arrived before their user-list entry.
|
|
||||||
* In that case locators may be missing, so angle/range analysis and DX spotting
|
|
||||||
* must be skipped while the chat message itself is still published.
|
|
||||||
*/
|
|
||||||
boolean senderHasLocator = newMessageArrived.getSender().getQra() != null
|
|
||||||
&& !newMessageArrived.getSender().getQra().isBlank();
|
|
||||||
boolean receiverHasLocator = newMessageArrived.getReceiver().getQra() != null
|
|
||||||
&& !newMessageArrived.getReceiver().getQra().isBlank();
|
|
||||||
|
|
||||||
if (senderHasLocator && receiverHasLocator) {
|
|
||||||
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
|
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
|
||||||
newMessageArrived.getSender().getQra(),
|
newMessageArrived.getSender().getQra(),
|
||||||
newMessageArrived.getReceiver().getQra(),
|
newMessageArrived.getReceiver().getQra(),
|
||||||
@@ -988,7 +998,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
|
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
|
||||||
try {
|
try {
|
||||||
if (newMessageArrived.getSender().getFrequency() != null) {
|
if (newMessageArrived.getSender().getFrequency() != null) {
|
||||||
//TODO: testing for next version 3.33: additional information will be displayed in cluster if there is such information
|
//TODO: testing for next version 3.33: addinitional information will be displayed in cluster if there is such an information
|
||||||
ChatMember onlyForSpottingObject = new ChatMember();
|
ChatMember onlyForSpottingObject = new ChatMember();
|
||||||
onlyForSpottingObject.setCallSign(newMessageArrived.getSender().getCallSign());
|
onlyForSpottingObject.setCallSign(newMessageArrived.getSender().getCallSign());
|
||||||
onlyForSpottingObject.setFrequency(newMessageArrived.getSender().getFrequency());
|
onlyForSpottingObject.setFrequency(newMessageArrived.getSender().getFrequency());
|
||||||
@@ -1004,10 +1014,11 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getPotential() + "%");
|
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getPotential() + "%");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra());
|
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject);
|
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject); //tells the DXCluster server to send a DXC message for this member to the logbook software
|
||||||
}
|
}
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
|
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
|
||||||
@@ -1025,21 +1036,14 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
" ; sender bearing to me: " + new Location(newMessageArrived.getSender().getQra()).getBearing(new Location(client.getChatPreferences().getStn_loginLocatorMainCat())));
|
" ; sender bearing to me: " + new Location(newMessageArrived.getSender().getQra()).getBearing(new Location(client.getChatPreferences().getStn_loginLocatorMainCat())));
|
||||||
newMessageArrived.getSender().setInAngleAndRange(false);
|
newMessageArrived.getSender().setInAngleAndRange(false);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
newMessageArrived.getSender().setInAngleAndRange(false);
|
|
||||||
System.out.println("[MSGBUSMGT, Info:] Skipping angle/range analysis for message with missing locator: "
|
|
||||||
+ newMessageArrived.getSender().getCallSign() + " -> "
|
|
||||||
+ newMessageArrived.getReceiver().getCallSign());
|
|
||||||
}
|
|
||||||
|
|
||||||
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
||||||
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
|
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
|
||||||
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
||||||
}
|
}
|
||||||
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
||||||
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: "
|
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: " + referenceDeletedByUserLeftChatDuringMessageprocessing.getStackTrace());
|
||||||
+ referenceDeletedByUserLeftChatDuringMessageprocessing.getMessage());
|
// referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
|
||||||
referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sdtout to me message-List
|
// sdtout to me message-List
|
||||||
@@ -1058,8 +1062,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
|
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
|
||||||
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
|
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
|
||||||
} catch (Exception exceptionOccured) {
|
} catch (Exception exceptionOccured) {
|
||||||
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage());
|
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
|
||||||
exceptionOccured.printStackTrace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
|
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
|
||||||
@@ -1082,21 +1085,24 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
temp4.setQra(splittedMessageLine[3]);
|
temp4.setQra(splittedMessageLine[3]);
|
||||||
temp4.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
temp4.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
||||||
|
|
||||||
ChatMember foundThisInChatMemberList = this.client.findActiveChatMember(temp4);
|
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), temp4);
|
||||||
if (foundThisInChatMemberList == null && this.client.getChatCategorySecondChat() != null) {
|
|
||||||
/*
|
if (index != -1) {
|
||||||
* LOC messages do not carry the category. The old implementation checked
|
|
||||||
* only the main category. Try the second active category as a fallback so
|
|
||||||
* dual-channel operation can still update locator changes.
|
|
||||||
*/
|
|
||||||
temp4.setChatCategory(this.client.getChatCategorySecondChat());
|
|
||||||
foundThisInChatMemberList = this.client.findActiveChatMember(temp4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundThisInChatMemberList != null) {
|
|
||||||
System.out.println("[MSGBUSMGT:] Locator Change of [" + (splittedMessageLine[2] + "], old was: "
|
System.out.println("[MSGBUSMGT:] Locator Change of [" + (splittedMessageLine[2] + "], old was: "
|
||||||
+ foundThisInChatMemberList.getQra() + " new is: " + splittedMessageLine[3]));
|
+ this.client.getLst_chatMemberList().get(index).getQra() + " new is: "
|
||||||
this.client.updateActiveChatMemberLocator(temp4, splittedMessageLine[3]);
|
+ splittedMessageLine[3]));
|
||||||
|
|
||||||
|
ChatMember foundThisInChatMemberList = this.client.getLst_chatMemberList().get(index); //make less list accesses
|
||||||
|
|
||||||
|
// this.client.getLst_chatMemberList().get(index).setQra(splittedMessageLine[3]);
|
||||||
|
// this.client.getLst_chatMemberList().get(index).setQrb(new Location().getDistanceKmByTwoLocatorStrings(client.getChatPreferences().getLoginLocator(), splittedMessageLine[3]));
|
||||||
|
// this.client.getLst_chatMemberList().get(index).setQTFdirection(new Location(client.getChatPreferences().getLoginLocator()).getBearing(new Location(splittedMessageLine[3])));
|
||||||
|
|
||||||
|
foundThisInChatMemberList.setQra(splittedMessageLine[3]);
|
||||||
|
foundThisInChatMemberList.setQrb(new Location().getDistanceKmByTwoLocatorStrings(client.getChatPreferences().getStn_loginLocatorMainCat(), splittedMessageLine[3]));
|
||||||
|
foundThisInChatMemberList.setQTFdirection(new Location(client.getChatPreferences().getStn_loginLocatorMainCat()).getBearing(new Location(splittedMessageLine[3])));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
System.out.println("[MSGBUSMGT:] ERROR! Locator Change of ["
|
System.out.println("[MSGBUSMGT:] ERROR! Locator Change of ["
|
||||||
+ (splittedMessageLine[2] + "] is not possible, user is not in the Table!"));
|
+ (splittedMessageLine[2] + "] is not possible, user is not in the Table!"));
|
||||||
@@ -1226,9 +1232,11 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
stateChangeMember.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
|
stateChangeMember.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
|
||||||
|
|
||||||
if (!this.client.updateActiveChatMemberState(stateChangeMember, stateChangeMember.getState())) {
|
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(),
|
||||||
System.out.println("[MSGBUSMGT, Info:] State change for inactive user: "
|
stateChangeMember);
|
||||||
+ stateChangeMember.getCallSign() + " / " + stateChangeMember.getChatCategory());
|
|
||||||
|
if (index != -1 && index != 0) {
|
||||||
|
this.client.getLst_chatMemberList().get(index).setState(stateChangeMember.getState());
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.client.getChatMemberTable().get(stateChangeMember.getCallSign())
|
// this.client.getChatMemberTable().get(stateChangeMember.getCallSign())
|
||||||
@@ -1241,43 +1249,31 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
*/
|
*/
|
||||||
if (splittedMessageLine[0].contains(USERINFOUPDATEORUSERISBACK)) {
|
if (splittedMessageLine[0].contains(USERINFOUPDATEORUSERISBACK)) {
|
||||||
|
|
||||||
if (splittedMessageLine.length < 6) {
|
|
||||||
System.out.println("[MSGBUSMGT, warning:] Malformed UM3 message ignored: "
|
|
||||||
+ messageToProcess.getMessageText());
|
|
||||||
} else {
|
|
||||||
ChatMember stateChangeMember = new ChatMember();
|
ChatMember stateChangeMember = new ChatMember();
|
||||||
|
|
||||||
stateChangeMember.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
|
stateChangeMember.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
|
||||||
|
|
||||||
stateChangeMember.setCallSign(splittedMessageLine[2]);
|
stateChangeMember.setCallSign(splittedMessageLine[2]);
|
||||||
stateChangeMember.setName(splittedMessageLine[3]);
|
stateChangeMember.setName(splittedMessageLine[3]);
|
||||||
stateChangeMember.setQra(splittedMessageLine[4]);
|
stateChangeMember.setQra(splittedMessageLine[4]);
|
||||||
stateChangeMember.setState(Integer.parseInt(splittedMessageLine[5]));
|
stateChangeMember.setState(Integer.parseInt(splittedMessageLine[5]));
|
||||||
stateChangeMember.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
stateChangeMember.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
||||||
stateChangeMember.setQrb(new Location().getDistanceKmByTwoLocatorStrings(
|
stateChangeMember.setQrb(new Location().getDistanceKmByTwoLocatorStrings(client.getChatPreferences().getStn_loginLocatorMainCat(), stateChangeMember.getQra()));
|
||||||
client.getChatPreferences().getStn_loginLocatorMainCat(),
|
stateChangeMember.setQTFdirection(new Location(client.getChatPreferences().getStn_loginLocatorMainCat()).getBearing(new Location(stateChangeMember.getQra())));
|
||||||
stateChangeMember.getQra()));
|
|
||||||
stateChangeMember.setQTFdirection(new Location(client.getChatPreferences().getStn_loginLocatorMainCat())
|
|
||||||
.getBearing(new Location(stateChangeMember.getQra())));
|
|
||||||
|
|
||||||
/*
|
this.client.getDbHandler().storeChatMember(stateChangeMember); // TODO: not clean, it should be an
|
||||||
* UM3 is only an info/profile update. ON4KST also sends it for stations
|
// upodate
|
||||||
* that are not logged into this channel. Such stations must not become
|
|
||||||
* visible chat members, otherwise the user could try to address an
|
// System.out.println("[MSGBUSMGT:] DXCluster Message detected ");
|
||||||
* offline callsign and receive server-side errors.
|
|
||||||
*
|
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(),
|
||||||
* Therefore UM3 updates existing active members only. Unknown UM3 calls
|
stateChangeMember);
|
||||||
* are intentionally ignored; if the station joins later, UA0/UA5/UA2
|
|
||||||
* will deliver the complete current user information again.
|
//-1 could be the case if mycall is processed
|
||||||
*/
|
if (index != -1) {
|
||||||
if (!client.getChatPreferences().getStn_loginCallSign().equalsIgnoreCase(stateChangeMember.getCallSign())) {
|
this.client.getLst_chatMemberList().get(index).setName(stateChangeMember.getName());
|
||||||
boolean updatedActiveMember = this.client.updateActiveChatMemberInfoIfPresent(stateChangeMember);
|
this.client.getLst_chatMemberList().get(index).setQra(stateChangeMember.getQra());
|
||||||
if (updatedActiveMember) {
|
this.client.getLst_chatMemberList().get(index).setState(stateChangeMember.getState());
|
||||||
this.client.getDbHandler().storeChatMember(stateChangeMember); // TODO: not clean, it should be an update
|
|
||||||
} else {
|
|
||||||
System.out.println("[MSGBUSMGT, Info:] UM3 ignored for inactive user: "
|
|
||||||
+ stateChangeMember.getCallSign() + " / " + stateChangeMember.getChatCategory());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
} else
|
||||||
@@ -1301,8 +1297,6 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
|
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
|
||||||
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
|
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
|
||||||
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
|
|
||||||
newMessageArrived.setMessageText(splittedMessageLine[6]);
|
|
||||||
|
|
||||||
if (splittedMessageLine[3].equals("SERVER")) {
|
if (splittedMessageLine[3].equals("SERVER")) {
|
||||||
ChatMember dummy = new ChatMember();
|
ChatMember dummy = new ChatMember();
|
||||||
@@ -1315,16 +1309,76 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
newMessageArrived.setSender(resolveInboundSender(
|
ChatMember sender = new ChatMember();
|
||||||
splittedMessageLine[3],
|
sender.setCallSign(splittedMessageLine[3]);
|
||||||
chategoryForMessageAndMessageSender,
|
sender.setChatCategory(chategoryForMessageAndMessageSender);
|
||||||
newMessageArrived));
|
|
||||||
|
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), sender);
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
//user not found in the chatmember list
|
||||||
|
try {
|
||||||
|
// newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
|
||||||
|
// this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
|
||||||
|
|
||||||
|
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
|
||||||
|
newMessageArrived.setSender(senderObj);
|
||||||
|
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
|
||||||
|
|
||||||
|
// Remember last inbound category per callsignRaw (required for correct send-routing later)
|
||||||
|
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
|
||||||
|
|
||||||
|
// Metrics for scoring: momentum, response-time, no-reply, positive signals
|
||||||
|
this.client.getStationMetricsService().onInboundMessage(
|
||||||
|
senderObj.getCallSignRaw(),
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
newMessageArrived.getMessageText(),
|
||||||
|
this.client.getChatPreferences(),
|
||||||
|
this.client.getChatPreferences().getStn_loginCallSign()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Activity/category changes influence priority => request recompute
|
||||||
|
this.client.getScoreService().requestRecompute("rx-chat-message");
|
||||||
|
|
||||||
|
} catch (Exception exc) {
|
||||||
|
ChatMember aSenderDummy = new ChatMember();
|
||||||
|
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
|
||||||
|
aSenderDummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
|
||||||
|
newMessageArrived.setSender(aSenderDummy);
|
||||||
|
System.out.println("MsgBusmgtT: Catched Error! " + exc.getMessage() + " // " + splittedMessageLine[3] + " is not in the list! Faking sender!");
|
||||||
|
exc.printStackTrace();
|
||||||
|
}
|
||||||
|
// b4 init list
|
||||||
|
} else {
|
||||||
|
//user not found in chatmember list, mark it, sender can not be set
|
||||||
|
if (!sender.getCallSign().equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
|
||||||
|
sender.setCallSign("[n/a]" + sender.getCallSign());
|
||||||
|
// if someone sent a message without being in the userlist (cause
|
||||||
|
// on4kst missed implementing....), callsign will be marked
|
||||||
|
} else {
|
||||||
|
//that means, message was by own station, broadcasted to all other
|
||||||
|
ChatMember dummy = new ChatMember();
|
||||||
|
dummy.setCallSign("ALL");
|
||||||
|
newMessageArrived.setReceiver(dummy);
|
||||||
|
|
||||||
|
AirPlaneReflectionInfo preventNullpointerExc = new AirPlaneReflectionInfo();
|
||||||
|
preventNullpointerExc.setAirPlanesReachableCntr(0);
|
||||||
|
sender.setAirPlaneReflectInfo(preventNullpointerExc);
|
||||||
|
newMessageArrived.setSender(sender); //my own call is the sender
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newMessageArrived.setSender(this.client.getChatMemberTable().get(splittedMessageLine[3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
|
||||||
|
newMessageArrived.setMessageText(splittedMessageLine[6]);
|
||||||
|
|
||||||
if (splittedMessageLine[7].equals("0")) {
|
if (splittedMessageLine[7].equals("0")) {
|
||||||
// message is not directed to anyone, move it to the cq messages.
|
// message is not directed to anyone, move it to the cq messages!
|
||||||
newMessageArrived.setReceiver(createAllReceiver());
|
ChatMember dummy = new ChatMember();
|
||||||
|
dummy.setCallSign("ALL");
|
||||||
|
newMessageArrived.setReceiver(dummy);
|
||||||
|
|
||||||
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
|
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
|
||||||
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
|
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
|
||||||
@@ -1332,9 +1386,35 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
} else {
|
} else {
|
||||||
//message is directed to another chatmember, process as such!
|
//message is directed to another chatmember, process as such!
|
||||||
|
|
||||||
newMessageArrived.setReceiver(resolveInboundReceiver(
|
ChatMember receiver = new ChatMember();
|
||||||
splittedMessageLine[7],
|
|
||||||
chategoryForMessageAndMessageSender));
|
receiver.setChatCategory(chategoryForMessageAndMessageSender); //got out of message itself
|
||||||
|
|
||||||
|
receiver.setCallSign(splittedMessageLine[7]);
|
||||||
|
|
||||||
|
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), receiver);
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
newMessageArrived.setReceiver(this.client.getLst_chatMemberList().get(index));// -1: Member left Chat
|
||||||
|
// before...
|
||||||
|
} else { //found in active member list
|
||||||
|
|
||||||
|
if (receiver.getCallSign().equals(client.getChatPreferences().getStn_loginCallSign())) {
|
||||||
|
/**
|
||||||
|
* If mycallsign sent a message to the server, server will publish that message and
|
||||||
|
* send it to all chatmember including me.
|
||||||
|
* As mycall is not in the userlist, the message would not been displayed if I handle
|
||||||
|
* it in the next case (marking left user, just for information). But I want an echo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
receiver.setCallSign(client.getChatPreferences().getStn_loginCallSign());
|
||||||
|
newMessageArrived.setReceiver(receiver);
|
||||||
|
} else {
|
||||||
|
//this are user which left chat but had been adressed by this message
|
||||||
|
receiver.setCallSign(receiver.getCallSign() + "(left)");
|
||||||
|
newMessageArrived.setReceiver(receiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
|
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
|
||||||
|
|
||||||
@@ -1367,16 +1447,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
//message sent to other user
|
//message sent to other user
|
||||||
// message sent from one other station to another other station
|
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
|
||||||
boolean senderHasLocator = newMessageArrived.getSender().getQra() != null
|
|
||||||
&& !newMessageArrived.getSender().getQra().isBlank();
|
|
||||||
|
|
||||||
boolean receiverHasLocator = newMessageArrived.getReceiver().getQra() != null
|
|
||||||
&& !newMessageArrived.getReceiver().getQra().isBlank();
|
|
||||||
|
|
||||||
if (senderHasLocator && receiverHasLocator) {
|
|
||||||
if (DirectionUtils.isInAngleAndRange(
|
|
||||||
client.getChatPreferences().getStn_loginLocatorMainCat(),
|
|
||||||
newMessageArrived.getSender().getQra(),
|
newMessageArrived.getSender().getQra(),
|
||||||
newMessageArrived.getReceiver().getQra(),
|
newMessageArrived.getReceiver().getQra(),
|
||||||
client.getChatPreferences().getStn_maxQRBDefault(),
|
client.getChatPreferences().getStn_maxQRBDefault(),
|
||||||
@@ -1385,24 +1456,17 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
newMessageArrived.getSender().setInAngleAndRange(true);
|
newMessageArrived.getSender().setInAngleAndRange(true);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
newMessageArrived.getSender().setInAngleAndRange(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
* Historic fallback senders/receivers may not have locators.
|
|
||||||
* The message is still valid and should be displayed, but angle/range
|
|
||||||
* analysis cannot be calculated without both locators.
|
|
||||||
*/
|
|
||||||
newMessageArrived.getSender().setInAngleAndRange(false);
|
newMessageArrived.getSender().setInAngleAndRange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.publishChatMessage(newMessageArrived);
|
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
||||||
|
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
|
||||||
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
||||||
}
|
}
|
||||||
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
||||||
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: "
|
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: " + referenceDeletedByUserLeftChatDuringMessageprocessing.getStackTrace());
|
||||||
+ referenceDeletedByUserLeftChatDuringMessageprocessing.getMessage());
|
// referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
|
||||||
referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sdtout to me message-List
|
// sdtout to me message-List
|
||||||
@@ -1415,8 +1479,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
|
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
|
||||||
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
|
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
|
||||||
} catch (Exception exceptionOccured) {
|
} catch (Exception exceptionOccured) {
|
||||||
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage());
|
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
|
||||||
exceptionOccured.printStackTrace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
|
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
|
||||||
@@ -1471,7 +1534,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
client.publishChatMessage(pwErrorMsg);
|
client.getLst_globalChatMessageList().add(pwErrorMsg);
|
||||||
// client.getLst_toMeMessageList().add(pwErrorMsg);
|
// client.getLst_toMeMessageList().add(pwErrorMsg);
|
||||||
// client.getLst_toAllMessageList().add(pwErrorMsg);
|
// client.getLst_toAllMessageList().add(pwErrorMsg);
|
||||||
}
|
}
|
||||||
@@ -1483,10 +1546,6 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
// this.client.disconnect();
|
// this.client.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (splittedMessageLine[0].equals("DE")) {
|
|
||||||
// DXCluster delimiter/end marker; intentionally ignored.
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
else {
|
||||||
|
|
||||||
// bufwrtrDBGMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
// bufwrtrDBGMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||||
@@ -1692,10 +1751,6 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
System.out.println("MsgBusMgt: process23001 went wrong / SQL Error");
|
System.out.println("MsgBusMgt: process23001 went wrong / SQL Error");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} catch (RuntimeException e) {
|
|
||||||
System.out.println("MsgBusMgt: process23001 went wrong / Runtime Error while processing: "
|
|
||||||
+ messageTextRaw.getMessageText());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="About KST4Contest">
|
<meta name="twitter:title" content="About KST4Contest">
|
||||||
<meta name="twitter:description" content="About KST4Contest and its contest-oriented ON4KST workflow.">
|
<meta name="twitter:description" content="About KST4Contest and its contest-oriented ON4KST workflow.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -85,7 +87,22 @@
|
|||||||
The project is open source and focused on practical contest station workflows.
|
The project is open source and focused on practical contest station workflows.
|
||||||
Features are designed around real operating pressure, not theoretical UI concepts.
|
Features are designed around real operating pressure, not theoretical UI concepts.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2>Community supported</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
KST4Contest is developed independently.
|
||||||
|
|
||||||
|
Community support helps finance hosting,
|
||||||
|
online services,
|
||||||
|
testing across multiple operating systems
|
||||||
|
and future contest-oriented features.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -109,6 +126,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Contact | KST4Contest">
|
<meta name="twitter:title" content="Contact | KST4Contest">
|
||||||
<meta name="twitter:description" content="Contact options for KST4Contest.">
|
<meta name="twitter:description" content="Contact options for KST4Contest.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -112,6 +114,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Download KST4Contest">
|
<meta name="twitter:title" content="Download KST4Contest">
|
||||||
<meta name="twitter:description" content="Download KST4Contest releases for Windows, Linux and macOS.">
|
<meta name="twitter:description" content="Download KST4Contest releases for Windows, Linux and macOS.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -225,6 +227,34 @@
|
|||||||
<a class="button secondary" href="/faq/">FAQ</a>
|
<a class="button secondary" href="/faq/">FAQ</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
|
||||||
|
<div class="card content-card">
|
||||||
|
|
||||||
|
<h2>Enjoying KST4Contest?</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
If KST4Contest makes contesting easier for you,
|
||||||
|
please consider supporting future development.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
|
||||||
|
<a class="button"
|
||||||
|
href="/support/">
|
||||||
|
|
||||||
|
❤️ Support Development
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -248,6 +278,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest FAQ">
|
<meta name="twitter:title" content="KST4Contest FAQ">
|
||||||
<meta name="twitter:description" content="Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.">
|
<meta name="twitter:description" content="Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -155,6 +157,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="AirScout Integration">
|
<meta name="twitter:title" content="AirScout Integration">
|
||||||
<meta name="twitter:description" content="AirScout information helps operators evaluate AP windows, aircraft scatter timing and candidate stations during VHF/UHF/SHF contests.">
|
<meta name="twitter:description" content="AirScout information helps operators evaluate AP windows, aircraft scatter timing and candidate stations during VHF/UHF/SHF contests.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -210,6 +212,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Dual Chat Categories">
|
<meta name="twitter:title" content="Dual Chat Categories">
|
||||||
<meta name="twitter:description" content="Dual category support gives contest operators better overview when working across multiple ON4KST chat rooms.">
|
<meta name="twitter:description" content="Dual category support gives contest operators better overview when working across multiple ON4KST chat rooms.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -186,6 +188,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="DXCluster Server">
|
<meta name="twitter:title" content="DXCluster Server">
|
||||||
<meta name="twitter:description" content="KST4Contest includes DXCluster functionality to support situational awareness during contest operation.">
|
<meta name="twitter:description" content="KST4Contest includes DXCluster functionality to support situational awareness during contest operation.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -185,6 +187,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest Features">
|
<meta name="twitter:title" content="KST4Contest Features">
|
||||||
<meta name="twitter:description" content="Contest-optimized ON4KST features for VHF, UHF and SHF operation.">
|
<meta name="twitter:description" content="Contest-optimized ON4KST features for VHF, UHF and SHF operation.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -171,6 +173,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Log Synchronization">
|
<meta name="twitter:title" content="Log Synchronization">
|
||||||
<meta name="twitter:description" content="Log synchronization helps KST4Contest understand worked stations, active bands and contest context.">
|
<meta name="twitter:description" content="Log synchronization helps KST4Contest understand worked stations, active bands and contest context.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -186,6 +188,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Macros and Variables">
|
<meta name="twitter:title" content="Macros and Variables">
|
||||||
<meta name="twitter:description" content="Macros reduce repetitive typing and help operators respond quickly during active ON4KST contest sessions.">
|
<meta name="twitter:description" content="Macros reduce repetitive typing and help operators respond quickly during active ON4KST contest sessions.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -185,6 +187,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Priority Score System">
|
<meta name="twitter:title" content="Priority Score System">
|
||||||
<meta name="twitter:description" content="KST4Contest ranks stations by activity, direction, sked context, band information, QRG hints and operator workflow signals.">
|
<meta name="twitter:description" content="KST4Contest ranks stations by activity, direction, sked context, band information, QRG hints and operator workflow signals.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -214,6 +216,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Sked Reminder">
|
<meta name="twitter:title" content="Sked Reminder">
|
||||||
<meta name="twitter:description" content="KST4Contest keeps skeds in focus while the operator handles chat traffic, logging, bands and aircraft scatter timing.">
|
<meta name="twitter:description" content="KST4Contest keeps skeds in focus while the operator handles chat traffic, logging, bands and aircraft scatter timing.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -208,6 +210,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Timeline View">
|
<meta name="twitter:title" content="Timeline View">
|
||||||
<meta name="twitter:description" content="The timeline view helps operators understand short propagation windows and candidate timing during active contest operation.">
|
<meta name="twitter:description" content="The timeline view helps operators understand short propagation windows and candidate timing during active contest operation.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -186,6 +188,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest – Contest-Optimized ON4KST Chat Client">
|
<meta name="twitter:title" content="KST4Contest – Contest-Optimized ON4KST Chat Client">
|
||||||
<meta name="twitter:description" content="KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.">
|
<meta name="twitter:description" content="KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -237,6 +239,7 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="button" href="/download/">Download KST4Contest</a>
|
<a class="button" href="/download/">Download KST4Contest</a>
|
||||||
<a class="button secondary" href="/features/">Explore features</a>
|
<a class="button secondary" href="/features/">Explore features</a>
|
||||||
|
<a class="button ghost" href="/support/">❤️ Support Development</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -275,6 +278,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Legal Notice | KST4Contest">
|
<meta name="twitter:title" content="Legal Notice | KST4Contest">
|
||||||
<meta name="twitter:description" content="Legal notice for the KST4Contest website.">
|
<meta name="twitter:description" content="Legal notice for the KST4Contest website.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -122,6 +124,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -196,6 +198,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -198,6 +200,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -333,6 +335,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -160,6 +162,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -282,6 +284,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -189,6 +191,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest Deutsches Handbuch">
|
<meta name="twitter:title" content="KST4Contest Deutsches Handbuch">
|
||||||
<meta name="twitter:description" content="Deutsches Benutzerhandbuch für KST4Contest.">
|
<meta name="twitter:description" content="Deutsches Benutzerhandbuch für KST4Contest.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -185,6 +187,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -304,6 +306,7 @@ flatpak install kst4contest de.x08.KST4Contest//nightly
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -240,6 +242,7 @@ Zudem wird die <strong>primäre Chat-Kategorie</strong> (z. B. IARU Region 1 VHF
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -218,6 +220,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -229,6 +231,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -196,6 +198,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -333,6 +335,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -240,6 +242,7 @@ Also, select the <strong>primary chat category</strong> (e.g., IARU Region 1 VHF
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -160,6 +162,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -281,6 +283,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -189,6 +191,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest English Manual">
|
<meta name="twitter:title" content="KST4Contest English Manual">
|
||||||
<meta name="twitter:description" content="English user manual for KST4Contest.">
|
<meta name="twitter:description" content="English user manual for KST4Contest.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -185,6 +187,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -304,6 +306,7 @@ flatpak install kst4contest de.x08.KST4Contest//nightly
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -218,6 +220,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -229,6 +231,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -198,6 +200,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest Manual">
|
<meta name="twitter:title" content="KST4Contest Manual">
|
||||||
<meta name="twitter:description" content="Online manual for KST4Contest, the contest-optimized ON4KST chat client.">
|
<meta name="twitter:description" content="Online manual for KST4Contest, the contest-optimized ON4KST chat client.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -97,6 +99,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Hotfix Version 1.41.1">
|
<meta name="twitter:title" content="Hotfix Version 1.41.1">
|
||||||
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
|
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -98,6 +100,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest Website Launch">
|
<meta name="twitter:title" content="KST4Contest Website Launch">
|
||||||
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
|
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -102,6 +104,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest News">
|
<meta name="twitter:title" content="KST4Contest News">
|
||||||
<meta name="twitter:description" content="News, release updates and development notes for KST4Contest.">
|
<meta name="twitter:description" content="News, release updates and development notes for KST4Contest.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -112,6 +114,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="Privacy Policy | KST4Contest">
|
<meta name="twitter:title" content="Privacy Policy | KST4Contest">
|
||||||
<meta name="twitter:description" content="Privacy policy for the KST4Contest website.">
|
<meta name="twitter:description" content="Privacy policy for the KST4Contest website.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -103,6 +105,22 @@
|
|||||||
GitHub is responsible for its own data processing.
|
GitHub is responsible for its own data processing.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
|
||||||
|
Support via Ko-fi
|
||||||
|
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
This website links to Ko-fi for voluntary financial support.
|
||||||
|
|
||||||
|
Payments are processed entirely by Ko-fi and its payment providers.
|
||||||
|
|
||||||
|
KST4Contest itself never receives or processes payment information.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>Your rights</h2>
|
<h2>Your rights</h2>
|
||||||
<p>
|
<p>
|
||||||
You may have the right to access, rectification, erasure, restriction of processing,
|
You may have the right to access, rectification, erasure, restriction of processing,
|
||||||
@@ -137,6 +155,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest Roadmap">
|
<meta name="twitter:title" content="KST4Contest Roadmap">
|
||||||
<meta name="twitter:description" content="Planned enhancements for upcoming KST4Contest versions, generated from GitHub milestones and issues.">
|
<meta name="twitter:description" content="Planned enhancements for upcoming KST4Contest versions, generated from GitHub milestones and issues.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -173,6 +175,48 @@
|
|||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
|
||||||
|
<div class="cta-panel">
|
||||||
|
|
||||||
|
<p class="eyebrow">
|
||||||
|
|
||||||
|
Community
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
|
||||||
|
Development is driven by the community.
|
||||||
|
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Many roadmap items require
|
||||||
|
time,
|
||||||
|
online services
|
||||||
|
and infrastructure.
|
||||||
|
|
||||||
|
Community support helps accelerate development.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
|
||||||
|
<a class="button"
|
||||||
|
|
||||||
|
href="/support/">
|
||||||
|
|
||||||
|
❤️ Support Development
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
@@ -195,6 +239,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="twitter:title" content="KST4Contest Screenshots">
|
<meta name="twitter:title" content="KST4Contest Screenshots">
|
||||||
<meta name="twitter:description" content="Screenshots of KST4Contest, the contest-optimized ON4KST chat client for VHF, UHF and SHF operators.">
|
<meta name="twitter:description" content="Screenshots of KST4Contest, the contest-optimized ON4KST chat client for VHF, UHF and SHF operators.">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
<a href="/faq/">FAQ</a>
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
<a class="nav-cta" href="/download/">Download</a>
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -123,6 +125,7 @@
|
|||||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
<p><a href="/contact/">Contact</a></p>
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+26
-22
@@ -10,90 +10,94 @@
|
|||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/about/</loc>
|
<loc>https://kst4contest.hamradioonline.de/about/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/contact/</loc>
|
<loc>https://kst4contest.hamradioonline.de/contact/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/download/</loc>
|
<loc>https://kst4contest.hamradioonline.de/download/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/faq/</loc>
|
<loc>https://kst4contest.hamradioonline.de/faq/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/airscout/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/airscout/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/dual-chat/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/dual-chat/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/dx-cluster/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/dx-cluster/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/log-sync/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/log-sync/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/macros/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/macros/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/priority-score/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/priority-score/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/sked-reminder/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/sked-reminder/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/features/timeline/</loc>
|
<loc>https://kst4contest.hamradioonline.de/features/timeline/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/</loc>
|
<loc>https://kst4contest.hamradioonline.de/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/legal-notice/</loc>
|
<loc>https://kst4contest.hamradioonline.de/legal-notice/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/manual/de/airscout-integration/</loc>
|
<loc>https://kst4contest.hamradioonline.de/manual/de/airscout-integration/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/manual/de/</loc>
|
<loc>https://kst4contest.hamradioonline.de/manual/de/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/manual/en/</loc>
|
<loc>https://kst4contest.hamradioonline.de/manual/en/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/manual/</loc>
|
<loc>https://kst4contest.hamradioonline.de/manual/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/privacy/</loc>
|
<loc>https://kst4contest.hamradioonline.de/privacy/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/roadmap/</loc>
|
<loc>https://kst4contest.hamradioonline.de/roadmap/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://kst4contest.hamradioonline.de/screenshots/</loc>
|
<loc>https://kst4contest.hamradioonline.de/screenshots/</loc>
|
||||||
<lastmod>2026-07-09</lastmod>
|
<lastmod>2026-07-11</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://kst4contest.hamradioonline.de/support/</loc>
|
||||||
|
<lastmod>2026-07-11</lastmod>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Support KST4Contest</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="Help support the ongoing development of KST4Contest.">
|
||||||
|
<link rel="canonical" href="https://kst4contest.hamradioonline.de/support/">
|
||||||
|
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="Support KST4Contest">
|
||||||
|
<meta property="og:description" content="Help support the ongoing development of KST4Contest.">
|
||||||
|
<meta property="og:url" content="https://kst4contest.hamradioonline.de/support/">
|
||||||
|
<meta property="og:site_name" content="KST4Contest">
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Support KST4Contest">
|
||||||
|
<meta name="twitter:description" content="Help support the ongoing development of KST4Contest.">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||||
|
|
||||||
|
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="site-bg"></div>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<span class="brand-mark">◎</span>
|
||||||
|
<span>
|
||||||
|
<strong>KST4Contest</strong>
|
||||||
|
<small>ON4KST Contest Client</small>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav class="site-nav">
|
||||||
|
|
||||||
|
<a href="/">Home</a>
|
||||||
|
|
||||||
|
<a href="/features/">Features</a>
|
||||||
|
|
||||||
|
<a href="/screenshots/">Screenshots</a>
|
||||||
|
|
||||||
|
<a href="/manual/">Manual</a>
|
||||||
|
|
||||||
|
<a href="/news/">News</a>
|
||||||
|
|
||||||
|
<a href="/roadmap/">Roadmap</a>
|
||||||
|
|
||||||
|
<a href="/about/">About</a>
|
||||||
|
|
||||||
|
<a href="/faq/">FAQ</a>
|
||||||
|
|
||||||
|
<a href="/support/">Support</a>
|
||||||
|
|
||||||
|
<a class="nav-cta" href="/download/">Download</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<p class="badge">Support Development</p>
|
||||||
|
|
||||||
|
<h1>Help shape the future of KST4Contest.</h1>
|
||||||
|
|
||||||
|
<p class="lead">
|
||||||
|
KST4Contest is an independent open-source project developed in my free time.
|
||||||
|
Your support helps cover infrastructure costs and enables new contest-oriented
|
||||||
|
features that would otherwise be difficult to implement.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a class="button"
|
||||||
|
href="https://ko-fi.com/praktimarc"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener">
|
||||||
|
❤️ Support on Ko-fi
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="button secondary"
|
||||||
|
href="https://github.com/praktimarc/kst4contest">
|
||||||
|
View Source Code
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
|
||||||
|
<div class="section-heading">
|
||||||
|
<p class="eyebrow">What previous support has enabled</p>
|
||||||
|
|
||||||
|
<h2>Real improvements funded by the community.</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
|
||||||
|
<h3>🛰 PSTRotator Integration</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Support helped justify the investment into rotor hardware and the required
|
||||||
|
PSTRotator license. Today KST4Contest can automatically control antennas
|
||||||
|
during contest operation.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
|
||||||
|
<h3>🗺 Interactive Map</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
The interactive station map required commercial map API usage.
|
||||||
|
Community support helped cover those API costs and made the feature possible.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
|
||||||
|
<div class="section-heading">
|
||||||
|
|
||||||
|
<p class="eyebrow">Future goals</p>
|
||||||
|
|
||||||
|
<h2>What future support will fund</h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
|
||||||
|
<h3>☁ Map API</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Keeping the online map service available requires ongoing API usage.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
|
||||||
|
<h3>📡 Tropo Server</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Future propagation prediction services and server-side calculations
|
||||||
|
will require dedicated hosting infrastructure.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
|
||||||
|
<h3>🔌 New Integrations</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Development of additional interfaces, logging software integrations,
|
||||||
|
contest tools and automation features.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
|
||||||
|
<h3>🚀 Faster Development</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
More development time means more contest-oriented features,
|
||||||
|
better testing and better documentation.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
|
||||||
|
<div class="cta-panel">
|
||||||
|
|
||||||
|
<p class="eyebrow">Every contribution helps</p>
|
||||||
|
|
||||||
|
<h2>Whether it's one coffee or ongoing support.</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Your support directly improves KST4Contest.
|
||||||
|
|
||||||
|
Unlike commercial software, every new feature is driven by real contest
|
||||||
|
experience and community feedback.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
|
||||||
|
<a class="button"
|
||||||
|
href="https://ko-fi.com/praktimarc"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener">
|
||||||
|
|
||||||
|
❤️ Support KST4Contest
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="footer-grid">
|
||||||
|
<div>
|
||||||
|
<strong>KST4Contest</strong>
|
||||||
|
<p>Contest-optimized ON4KST workflow for VHF/UHF/SHF operators.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>Project</strong>
|
||||||
|
<p><a href="/features/">Features</a></p>
|
||||||
|
<p><a href="/download/">Download</a></p>
|
||||||
|
<p><a href="/manual/">Manual</a></p>
|
||||||
|
<p><a href="/roadmap/">Roadmap</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>Community</strong>
|
||||||
|
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||||
|
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||||
|
<p><a href="/contact/">Contact</a></p>
|
||||||
|
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>Legal</strong>
|
||||||
|
<p><a href="/legal-notice/">Legal Notice</a></p>
|
||||||
|
<p><a href="/privacy/">Privacy Policy</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -81,10 +81,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{% if heroFx %}
|
|
||||||
<script src="/assets/js/hero-radio-fx.js?v={{ build.version }}" defer></script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -7,22 +7,10 @@
|
|||||||
--text: #f8fafc;
|
--text: #f8fafc;
|
||||||
--muted: #aab6ca;
|
--muted: #aab6ca;
|
||||||
--soft: #64748b;
|
--soft: #64748b;
|
||||||
/*--accent: #38bdf8;*/
|
--accent: #38bdf8;
|
||||||
/*--accent2: #a855f7;*/
|
--accent2: #a855f7;
|
||||||
/*--accent3: #22c55e;*/
|
--accent3: #22c55e;
|
||||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
|
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
|
||||||
|
|
||||||
--accent: #54d63d;
|
|
||||||
--accent2: #8aef64;
|
|
||||||
--accent3: #31b54a;
|
|
||||||
|
|
||||||
--fx-plane: #79ff73;
|
|
||||||
--fx-plane-muted: rgba(121, 255, 115, 0.42);
|
|
||||||
--fx-beam: #52ff65;
|
|
||||||
--fx-beam-glow: rgba(82, 255, 101, 0.28);
|
|
||||||
--fx-reflection: #d2ffc8;
|
|
||||||
--fx-trail: rgba(121, 255, 115, 0.17);
|
|
||||||
--fx-grid: rgba(121, 255, 115, 0.055);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -37,39 +25,13 @@ html {
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
radial-gradient(circle at 14% 12%, rgba(168, 85, 247, 0.30), transparent 28%),
|
||||||
circle at 14% 12%,
|
radial-gradient(circle at 82% 16%, rgba(56, 189, 248, 0.24), transparent 30%),
|
||||||
rgba(52, 165, 57, 0.20),
|
radial-gradient(circle at 50% 85%, rgba(34, 197, 94, 0.11), transparent 35%),
|
||||||
transparent 30%
|
linear-gradient(180deg, #050816 0%, #07111f 100%);
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 82% 16%,
|
|
||||||
rgba(95, 218, 82, 0.14),
|
|
||||||
transparent 32%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 50% 85%,
|
|
||||||
rgba(32, 126, 55, 0.12),
|
|
||||||
transparent 36%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
180deg,
|
|
||||||
#070b09 0%,
|
|
||||||
#0b1410 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family:
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
Inter,
|
|
||||||
ui-sans-serif,
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
sans-serif;
|
|
||||||
|
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
@@ -600,155 +562,3 @@ a:hover {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Interactive radio / aircraft-scatter hero layer
|
|
||||||
*/
|
|
||||||
|
|
||||||
.hero-radio-fx {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.76;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-fx-vignette {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
background:
|
|
||||||
linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(7, 11, 9, 0.92) 0%,
|
|
||||||
rgba(7, 11, 9, 0.68) 34%,
|
|
||||||
rgba(7, 11, 9, 0.16) 64%,
|
|
||||||
rgba(7, 11, 9, 0.42) 100%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(7, 11, 9, 0.10),
|
|
||||||
rgba(7, 11, 9, 0.58)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-split > *:not(.hero-radio-fx):not(.hero-fx-vignette) {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Operator-console badge styling
|
|
||||||
*/
|
|
||||||
|
|
||||||
.badge,
|
|
||||||
.eyebrow {
|
|
||||||
color: #caffc0;
|
|
||||||
background: rgba(52, 165, 57, 0.12);
|
|
||||||
border-color: rgba(105, 232, 88, 0.32);
|
|
||||||
box-shadow: 0 0 22px rgba(82, 255, 101, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Light-mode identity:
|
|
||||||
* white/grey UI with red contest accents
|
|
||||||
*/
|
|
||||||
|
|
||||||
html[data-theme="light"] {
|
|
||||||
--bg: #f4f4f2;
|
|
||||||
--bg2: #ffffff;
|
|
||||||
--panel: rgba(255, 255, 255, 0.88);
|
|
||||||
--panel2: rgba(242, 242, 239, 0.92);
|
|
||||||
--border: rgba(91, 91, 86, 0.20);
|
|
||||||
--text: #252522;
|
|
||||||
--muted: #656560;
|
|
||||||
--soft: #888881;
|
|
||||||
|
|
||||||
--accent: #d33732;
|
|
||||||
--accent2: #f25a51;
|
|
||||||
--accent3: #ae2727;
|
|
||||||
|
|
||||||
--fx-plane: #df3732;
|
|
||||||
--fx-plane-muted: rgba(211, 55, 50, 0.38);
|
|
||||||
--fx-beam: #ef4039;
|
|
||||||
--fx-beam-glow: rgba(239, 64, 57, 0.22);
|
|
||||||
--fx-reflection: #ff8d84;
|
|
||||||
--fx-trail: rgba(211, 55, 50, 0.15);
|
|
||||||
--fx-grid: rgba(185, 47, 43, 0.045);
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="light"] body {
|
|
||||||
background:
|
|
||||||
radial-gradient(
|
|
||||||
circle at 14% 12%,
|
|
||||||
rgba(211, 55, 50, 0.12),
|
|
||||||
transparent 30%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 82% 16%,
|
|
||||||
rgba(242, 90, 81, 0.09),
|
|
||||||
transparent 32%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
180deg,
|
|
||||||
#ffffff 0%,
|
|
||||||
#f0f0ed 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="light"] .hero-fx-vignette {
|
|
||||||
background:
|
|
||||||
linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(255, 255, 255, 0.95) 0%,
|
|
||||||
rgba(255, 255, 255, 0.76) 34%,
|
|
||||||
rgba(255, 255, 255, 0.18) 66%,
|
|
||||||
rgba(255, 255, 255, 0.48) 100%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(255, 255, 255, 0.06),
|
|
||||||
rgba(240, 240, 237, 0.62)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="light"] .badge,
|
|
||||||
html[data-theme="light"] .eyebrow {
|
|
||||||
color: #962a27;
|
|
||||||
background: rgba(211, 55, 50, 0.08);
|
|
||||||
border-color: rgba(211, 55, 50, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.hero-radio-fx {
|
|
||||||
opacity: 0.48;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-fx-vignette {
|
|
||||||
background:
|
|
||||||
linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(7, 11, 9, 0.82),
|
|
||||||
rgba(7, 11, 9, 0.34)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="light"] .hero-fx-vignette {
|
|
||||||
background:
|
|
||||||
linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(255, 255, 255, 0.90),
|
|
||||||
rgba(255, 255, 255, 0.42)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.hero-radio-fx {
|
|
||||||
opacity: 0.28;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,710 +0,0 @@
|
|||||||
(() => {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const canvas = document.querySelector("[data-hero-radio-fx]");
|
|
||||||
|
|
||||||
if (!(canvas instanceof HTMLCanvasElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = canvas.getContext("2d", {
|
|
||||||
alpha: true,
|
|
||||||
desynchronized: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hero = canvas.closest(".hero");
|
|
||||||
|
|
||||||
if (!(hero instanceof HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reducedMotion = window.matchMedia(
|
|
||||||
"(prefers-reduced-motion: reduce)"
|
|
||||||
);
|
|
||||||
|
|
||||||
const coarsePointer = window.matchMedia(
|
|
||||||
"(pointer: coarse)"
|
|
||||||
);
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
dpr: 1,
|
|
||||||
running: false,
|
|
||||||
visible: true,
|
|
||||||
lastFrame: 0,
|
|
||||||
spawnAccumulator: 0,
|
|
||||||
beamAccumulator: 0,
|
|
||||||
animationFrameId: 0,
|
|
||||||
pointer: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
planes: [],
|
|
||||||
beams: [],
|
|
||||||
reflections: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const clamp = (value, minimum, maximum) =>
|
|
||||||
Math.max(minimum, Math.min(maximum, value));
|
|
||||||
|
|
||||||
const randomBetween = (minimum, maximum) =>
|
|
||||||
minimum + Math.random() * (maximum - minimum);
|
|
||||||
|
|
||||||
function getCssColor(name, fallback) {
|
|
||||||
const value = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue(name)
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
return value || fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPalette() {
|
|
||||||
return {
|
|
||||||
plane: getCssColor("--fx-plane", "#79ff73"),
|
|
||||||
planeMuted: getCssColor("--fx-plane-muted", "rgba(121,255,115,.42)"),
|
|
||||||
beam: getCssColor("--fx-beam", "#52ff65"),
|
|
||||||
beamGlow: getCssColor("--fx-beam-glow", "rgba(82,255,101,.28)"),
|
|
||||||
reflection: getCssColor("--fx-reflection", "#c8ffbf"),
|
|
||||||
trail: getCssColor("--fx-trail", "rgba(121,255,115,.16)"),
|
|
||||||
grid: getCssColor("--fx-grid", "rgba(121,255,115,.055)")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeCanvas() {
|
|
||||||
const bounds = hero.getBoundingClientRect();
|
|
||||||
|
|
||||||
state.width = Math.max(1, Math.round(bounds.width));
|
|
||||||
state.height = Math.max(1, Math.round(bounds.height));
|
|
||||||
state.dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
||||||
|
|
||||||
canvas.width = Math.round(state.width * state.dpr);
|
|
||||||
canvas.height = Math.round(state.height * state.dpr);
|
|
||||||
|
|
||||||
canvas.style.width = `${state.width}px`;
|
|
||||||
canvas.style.height = `${state.height}px`;
|
|
||||||
|
|
||||||
context.setTransform(
|
|
||||||
state.dpr,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
state.dpr,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!state.pointer.active) {
|
|
||||||
state.pointer.x = state.width * 0.54;
|
|
||||||
state.pointer.y = state.height * 0.42;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlaneLimit() {
|
|
||||||
if (coarsePointer.matches) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.width < 900) {
|
|
||||||
return 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPlane() {
|
|
||||||
const margin = 55;
|
|
||||||
const edge = Math.floor(Math.random() * 4);
|
|
||||||
|
|
||||||
let x;
|
|
||||||
let y;
|
|
||||||
|
|
||||||
switch (edge) {
|
|
||||||
case 0:
|
|
||||||
x = randomBetween(0, state.width);
|
|
||||||
y = -margin;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
x = state.width + margin;
|
|
||||||
y = randomBetween(0, state.height);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
x = randomBetween(0, state.width);
|
|
||||||
y = state.height + margin;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
x = -margin;
|
|
||||||
y = randomBetween(0, state.height);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetX = state.pointer.active
|
|
||||||
? state.pointer.x
|
|
||||||
: state.width * randomBetween(0.38, 0.72);
|
|
||||||
|
|
||||||
const targetY = state.pointer.active
|
|
||||||
? state.pointer.y
|
|
||||||
: state.height * randomBetween(0.2, 0.72);
|
|
||||||
|
|
||||||
const angle = Math.atan2(targetY - y, targetX - x);
|
|
||||||
const speed = randomBetween(18, 34);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
previousX: x,
|
|
||||||
previousY: y,
|
|
||||||
velocityX: Math.cos(angle) * speed,
|
|
||||||
velocityY: Math.sin(angle) * speed,
|
|
||||||
angle,
|
|
||||||
size: randomBetween(5.5, 9),
|
|
||||||
age: 0,
|
|
||||||
maximumAge: randomBetween(12, 20),
|
|
||||||
steering: randomBetween(0.65, 1.2),
|
|
||||||
wobble: randomBetween(0, Math.PI * 2),
|
|
||||||
wobbleSpeed: randomBetween(0.7, 1.4),
|
|
||||||
hit: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePlane(plane, deltaSeconds) {
|
|
||||||
plane.age += deltaSeconds;
|
|
||||||
plane.wobble += deltaSeconds * plane.wobbleSpeed;
|
|
||||||
|
|
||||||
plane.previousX = plane.x;
|
|
||||||
plane.previousY = plane.y;
|
|
||||||
|
|
||||||
const targetX = state.pointer.active
|
|
||||||
? state.pointer.x
|
|
||||||
: state.width * 0.56;
|
|
||||||
|
|
||||||
const targetY = state.pointer.active
|
|
||||||
? state.pointer.y
|
|
||||||
: state.height * 0.44;
|
|
||||||
|
|
||||||
const desiredAngle = Math.atan2(
|
|
||||||
targetY - plane.y,
|
|
||||||
targetX - plane.x
|
|
||||||
);
|
|
||||||
|
|
||||||
let angleDifference = desiredAngle - plane.angle;
|
|
||||||
|
|
||||||
while (angleDifference > Math.PI) {
|
|
||||||
angleDifference -= Math.PI * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (angleDifference < -Math.PI) {
|
|
||||||
angleDifference += Math.PI * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
plane.angle += angleDifference * plane.steering * deltaSeconds;
|
|
||||||
|
|
||||||
const speed = Math.hypot(
|
|
||||||
plane.velocityX,
|
|
||||||
plane.velocityY
|
|
||||||
);
|
|
||||||
|
|
||||||
const wobble = Math.sin(plane.wobble) * 0.14;
|
|
||||||
|
|
||||||
plane.velocityX =
|
|
||||||
Math.cos(plane.angle + wobble) * speed;
|
|
||||||
|
|
||||||
plane.velocityY =
|
|
||||||
Math.sin(plane.angle + wobble) * speed;
|
|
||||||
|
|
||||||
plane.x += plane.velocityX * deltaSeconds;
|
|
||||||
plane.y += plane.velocityY * deltaSeconds;
|
|
||||||
|
|
||||||
plane.hit = Math.max(0, plane.hit - deltaSeconds * 1.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlaneExpired(plane) {
|
|
||||||
const margin = 180;
|
|
||||||
|
|
||||||
return (
|
|
||||||
plane.age > plane.maximumAge ||
|
|
||||||
plane.x < -margin ||
|
|
||||||
plane.x > state.width + margin ||
|
|
||||||
plane.y < -margin ||
|
|
||||||
plane.y > state.height + margin
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseBeamTarget() {
|
|
||||||
if (state.planes.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetX = state.pointer.active
|
|
||||||
? state.pointer.x
|
|
||||||
: state.width * 0.56;
|
|
||||||
|
|
||||||
const targetY = state.pointer.active
|
|
||||||
? state.pointer.y
|
|
||||||
: state.height * 0.44;
|
|
||||||
|
|
||||||
const candidates = state.planes
|
|
||||||
.map((plane) => ({
|
|
||||||
plane,
|
|
||||||
distance: Math.hypot(
|
|
||||||
plane.x - targetX,
|
|
||||||
plane.y - targetY
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.filter((entry) => entry.distance < 260)
|
|
||||||
.sort((a, b) => a.distance - b.distance);
|
|
||||||
|
|
||||||
if (candidates.length > 0) {
|
|
||||||
return candidates[0].plane;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.planes[
|
|
||||||
Math.floor(Math.random() * state.planes.length)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBeam(target) {
|
|
||||||
const leftStation = {
|
|
||||||
x: state.width * 0.12,
|
|
||||||
y: state.height * 0.82
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightStation = {
|
|
||||||
x: state.width * 0.88,
|
|
||||||
y: state.height * 0.78
|
|
||||||
};
|
|
||||||
|
|
||||||
const source =
|
|
||||||
Math.abs(target.x - leftStation.x) <
|
|
||||||
Math.abs(target.x - rightStation.x)
|
|
||||||
? leftStation
|
|
||||||
: rightStation;
|
|
||||||
|
|
||||||
state.beams.push({
|
|
||||||
startX: source.x,
|
|
||||||
startY: source.y,
|
|
||||||
endX: target.x,
|
|
||||||
endY: target.y,
|
|
||||||
age: 0,
|
|
||||||
maximumAge: 0.62
|
|
||||||
});
|
|
||||||
|
|
||||||
target.hit = 1;
|
|
||||||
|
|
||||||
state.reflections.push({
|
|
||||||
x: target.x,
|
|
||||||
y: target.y,
|
|
||||||
age: 0,
|
|
||||||
maximumAge: 0.9,
|
|
||||||
radius: randomBetween(8, 15)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateEffects(deltaSeconds) {
|
|
||||||
for (const beam of state.beams) {
|
|
||||||
beam.age += deltaSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.beams = state.beams.filter(
|
|
||||||
(beam) => beam.age < beam.maximumAge
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const reflection of state.reflections) {
|
|
||||||
reflection.age += deltaSeconds;
|
|
||||||
reflection.radius += deltaSeconds * 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.reflections = state.reflections.filter(
|
|
||||||
(reflection) =>
|
|
||||||
reflection.age < reflection.maximumAge
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawGrid(palette) {
|
|
||||||
context.save();
|
|
||||||
|
|
||||||
context.strokeStyle = palette.grid;
|
|
||||||
context.lineWidth = 1;
|
|
||||||
|
|
||||||
const spacing = 54;
|
|
||||||
|
|
||||||
for (
|
|
||||||
let x = spacing;
|
|
||||||
x < state.width;
|
|
||||||
x += spacing
|
|
||||||
) {
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(x, 0);
|
|
||||||
context.lineTo(x, state.height);
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (
|
|
||||||
let y = spacing;
|
|
||||||
y < state.height;
|
|
||||||
y += spacing
|
|
||||||
) {
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(0, y);
|
|
||||||
context.lineTo(state.width, y);
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawRadarTarget(palette) {
|
|
||||||
const x = state.pointer.active
|
|
||||||
? state.pointer.x
|
|
||||||
: state.width * 0.56;
|
|
||||||
|
|
||||||
const y = state.pointer.active
|
|
||||||
? state.pointer.y
|
|
||||||
: state.height * 0.44;
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
|
|
||||||
context.strokeStyle = palette.planeMuted;
|
|
||||||
context.lineWidth = 1;
|
|
||||||
|
|
||||||
for (const radius of [24, 48, 82]) {
|
|
||||||
context.globalAlpha = 0.24 - radius / 500;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.globalAlpha = 0.2;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(x - 100, y);
|
|
||||||
context.lineTo(x + 100, y);
|
|
||||||
context.moveTo(x, y - 100);
|
|
||||||
context.lineTo(x, y + 100);
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawPlane(plane, palette) {
|
|
||||||
context.save();
|
|
||||||
|
|
||||||
context.translate(plane.x, plane.y);
|
|
||||||
context.rotate(plane.angle);
|
|
||||||
|
|
||||||
const alpha = clamp(
|
|
||||||
Math.min(
|
|
||||||
plane.age * 1.8,
|
|
||||||
(plane.maximumAge - plane.age) * 1.8
|
|
||||||
),
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
context.globalAlpha = alpha;
|
|
||||||
|
|
||||||
context.strokeStyle = palette.trail;
|
|
||||||
context.lineWidth = 1;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(-plane.size * 4.5, 0);
|
|
||||||
context.lineTo(-plane.size * 1.4, 0);
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
context.fillStyle =
|
|
||||||
plane.hit > 0
|
|
||||||
? palette.reflection
|
|
||||||
: palette.plane;
|
|
||||||
|
|
||||||
context.shadowColor =
|
|
||||||
plane.hit > 0
|
|
||||||
? palette.reflection
|
|
||||||
: palette.beamGlow;
|
|
||||||
|
|
||||||
context.shadowBlur =
|
|
||||||
plane.hit > 0
|
|
||||||
? 16
|
|
||||||
: 5;
|
|
||||||
|
|
||||||
const size = plane.size;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
|
|
||||||
context.moveTo(size * 1.65, 0);
|
|
||||||
context.lineTo(-size * 0.7, size * 0.34);
|
|
||||||
context.lineTo(-size * 0.15, size * 1.05);
|
|
||||||
context.lineTo(-size * 0.65, size * 1.08);
|
|
||||||
context.lineTo(-size * 1.35, size * 0.3);
|
|
||||||
context.lineTo(-size * 1.7, size * 0.28);
|
|
||||||
context.lineTo(-size * 1.12, 0);
|
|
||||||
context.lineTo(-size * 1.7, -size * 0.28);
|
|
||||||
context.lineTo(-size * 1.35, -size * 0.3);
|
|
||||||
context.lineTo(-size * 0.65, -size * 1.08);
|
|
||||||
context.lineTo(-size * 0.15, -size * 1.05);
|
|
||||||
context.lineTo(-size * 0.7, -size * 0.34);
|
|
||||||
|
|
||||||
context.closePath();
|
|
||||||
context.fill();
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawBeam(beam, palette) {
|
|
||||||
const progress = beam.age / beam.maximumAge;
|
|
||||||
const alpha = Math.sin(progress * Math.PI);
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
|
|
||||||
context.globalAlpha = alpha;
|
|
||||||
context.strokeStyle = palette.beamGlow;
|
|
||||||
context.lineWidth = 6;
|
|
||||||
context.shadowColor = palette.beam;
|
|
||||||
context.shadowBlur = 15;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(beam.startX, beam.startY);
|
|
||||||
context.lineTo(beam.endX, beam.endY);
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
context.strokeStyle = palette.beam;
|
|
||||||
context.lineWidth = 1.4;
|
|
||||||
context.shadowBlur = 5;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(beam.startX, beam.startY);
|
|
||||||
context.lineTo(beam.endX, beam.endY);
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawReflection(reflection, palette) {
|
|
||||||
const progress =
|
|
||||||
reflection.age / reflection.maximumAge;
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
|
|
||||||
context.globalAlpha = 1 - progress;
|
|
||||||
context.strokeStyle = palette.reflection;
|
|
||||||
context.lineWidth = 1.5;
|
|
||||||
context.shadowColor = palette.beam;
|
|
||||||
context.shadowBlur = 12;
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.arc(
|
|
||||||
reflection.x,
|
|
||||||
reflection.y,
|
|
||||||
reflection.radius,
|
|
||||||
0,
|
|
||||||
Math.PI * 2
|
|
||||||
);
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawStaticScene() {
|
|
||||||
const palette = getPalette();
|
|
||||||
|
|
||||||
context.clearRect(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
state.width,
|
|
||||||
state.height
|
|
||||||
);
|
|
||||||
|
|
||||||
drawGrid(palette);
|
|
||||||
drawRadarTarget(palette);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawScene() {
|
|
||||||
const palette = getPalette();
|
|
||||||
|
|
||||||
context.clearRect(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
state.width,
|
|
||||||
state.height
|
|
||||||
);
|
|
||||||
|
|
||||||
drawGrid(palette);
|
|
||||||
drawRadarTarget(palette);
|
|
||||||
|
|
||||||
for (const plane of state.planes) {
|
|
||||||
drawPlane(plane, palette);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const beam of state.beams) {
|
|
||||||
drawBeam(beam, palette);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const reflection of state.reflections) {
|
|
||||||
drawReflection(reflection, palette);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate(timestamp) {
|
|
||||||
if (!state.running) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deltaSeconds = clamp(
|
|
||||||
(timestamp - state.lastFrame) / 1000 || 0,
|
|
||||||
0,
|
|
||||||
0.05
|
|
||||||
);
|
|
||||||
|
|
||||||
state.lastFrame = timestamp;
|
|
||||||
|
|
||||||
if (!state.visible || document.hidden) {
|
|
||||||
state.animationFrameId =
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.spawnAccumulator += deltaSeconds;
|
|
||||||
state.beamAccumulator += deltaSeconds;
|
|
||||||
|
|
||||||
const planeLimit = getPlaneLimit();
|
|
||||||
const spawnInterval =
|
|
||||||
coarsePointer.matches ? 2.2 : 1.15;
|
|
||||||
|
|
||||||
if (
|
|
||||||
state.spawnAccumulator >= spawnInterval &&
|
|
||||||
state.planes.length < planeLimit
|
|
||||||
) {
|
|
||||||
state.spawnAccumulator = 0;
|
|
||||||
state.planes.push(createPlane());
|
|
||||||
}
|
|
||||||
|
|
||||||
const beamInterval =
|
|
||||||
coarsePointer.matches ? 3.3 : 1.8;
|
|
||||||
|
|
||||||
if (
|
|
||||||
state.beamAccumulator >= beamInterval &&
|
|
||||||
state.planes.length > 0
|
|
||||||
) {
|
|
||||||
state.beamAccumulator = 0;
|
|
||||||
|
|
||||||
const target = chooseBeamTarget();
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
createBeam(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const plane of state.planes) {
|
|
||||||
updatePlane(plane, deltaSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.planes = state.planes.filter(
|
|
||||||
(plane) => !isPlaneExpired(plane)
|
|
||||||
);
|
|
||||||
|
|
||||||
updateEffects(deltaSeconds);
|
|
||||||
drawScene();
|
|
||||||
|
|
||||||
state.animationFrameId =
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function start() {
|
|
||||||
cancelAnimationFrame(state.animationFrameId);
|
|
||||||
|
|
||||||
resizeCanvas();
|
|
||||||
|
|
||||||
if (reducedMotion.matches) {
|
|
||||||
state.running = false;
|
|
||||||
drawStaticScene();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.running = true;
|
|
||||||
state.lastFrame = performance.now();
|
|
||||||
|
|
||||||
if (state.planes.length === 0) {
|
|
||||||
const initialPlanes =
|
|
||||||
coarsePointer.matches ? 2 : 5;
|
|
||||||
|
|
||||||
for (let index = 0; index < initialPlanes; index++) {
|
|
||||||
const plane = createPlane();
|
|
||||||
plane.age = Math.random() * 2;
|
|
||||||
state.planes.push(plane);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.animationFrameId =
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePointer(event) {
|
|
||||||
const bounds = hero.getBoundingClientRect();
|
|
||||||
|
|
||||||
state.pointer.x = clamp(
|
|
||||||
event.clientX - bounds.left,
|
|
||||||
0,
|
|
||||||
bounds.width
|
|
||||||
);
|
|
||||||
|
|
||||||
state.pointer.y = clamp(
|
|
||||||
event.clientY - bounds.top,
|
|
||||||
0,
|
|
||||||
bounds.height
|
|
||||||
);
|
|
||||||
|
|
||||||
state.pointer.active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
hero.addEventListener("pointermove", updatePointer, {
|
|
||||||
passive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
hero.addEventListener("pointerleave", () => {
|
|
||||||
state.pointer.active = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("resize", resizeCanvas, {
|
|
||||||
passive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
state.lastFrame = performance.now();
|
|
||||||
});
|
|
||||||
|
|
||||||
reducedMotion.addEventListener("change", start);
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
resizeCanvas();
|
|
||||||
|
|
||||||
if (reducedMotion.matches) {
|
|
||||||
drawStaticScene();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
resizeObserver.observe(hero);
|
|
||||||
|
|
||||||
const intersectionObserver = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
state.visible =
|
|
||||||
entries[0]?.isIntersecting ?? true;
|
|
||||||
|
|
||||||
state.lastFrame = performance.now();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.05
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
intersectionObserver.observe(hero);
|
|
||||||
|
|
||||||
start();
|
|
||||||
})();
|
|
||||||
@@ -2,18 +2,9 @@
|
|||||||
layout: base.njk
|
layout: base.njk
|
||||||
title: KST4Contest – Contest-Optimized ON4KST Chat Client
|
title: KST4Contest – Contest-Optimized ON4KST Chat Client
|
||||||
description: KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.
|
description: KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.
|
||||||
heroFx: true
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="hero hero-split">
|
<section class="hero hero-split">
|
||||||
|
|
||||||
<canvas
|
|
||||||
class="hero-radio-fx"
|
|
||||||
data-hero-radio-fx
|
|
||||||
aria-hidden="true">
|
|
||||||
</canvas>
|
|
||||||
|
|
||||||
<div class="hero-fx-vignette" aria-hidden="true"></div>
|
|
||||||
<div>
|
<div>
|
||||||
<p class="badge">The contest-optimized ON4KST client</p>
|
<p class="badge">The contest-optimized ON4KST client</p>
|
||||||
<h1>KST4Contest</h1>
|
<h1>KST4Contest</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user