Initialier Commit

This commit is contained in:
2026-04-19 19:53:02 +02:00
commit 29da89c36e
566 changed files with 56561 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<queries>
<package android:name="deezer.android.app" />
<package android:name="com.apple.android.music" />
<package android:name="com.spotify.music" />
<package android:name="com.amazon.mp3" />
<package android:name="com.aspiro.tidal" />
<package android:name="com.google.android.youtube" />
<package android:name="com.google.android.apps.youtube.music" />
<package android:name="com.soundcloud.android" />
</queries>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:extractNativeLibs="false"
android:supportsRtl="true"
android:theme="@style/Theme.Linkster">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ScanActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".GameActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application>
</manifest>

View File

@@ -0,0 +1,61 @@
package de.oaa.linkster;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.oaa.linkster.model.SongResult;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class ApiClient {
private static final String BASE_URL = "https://linkster.langhei.de/api";
private static ApiClient instance;
private final OkHttpClient http = new OkHttpClient();
public static ApiClient getInstance() {
if (instance == null) instance = new ApiClient();
return instance;
}
public SongResult resolve(String scannedUrl) throws IOException {
HttpUrl url = HttpUrl.parse(BASE_URL + "/resolve").newBuilder()
.addQueryParameter("url", scannedUrl)
.build();
Request request = new Request.Builder().url(url).build();
try (Response response = http.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Server-Fehler: " + response.code());
}
String body = response.body().string();
return parse(body);
}
}
private SongResult parse(String json) {
JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
if (!obj.get("found").getAsBoolean()) return null;
String title = obj.get("title").getAsString();
String artist = obj.get("artist").getAsString();
String playlist = obj.get("playlist").getAsString();
Map<String, String> providers = new LinkedHashMap<>();
JsonObject prov = obj.getAsJsonObject("providers");
if (prov != null) {
for (Map.Entry<String, com.google.gson.JsonElement> e : prov.entrySet()) {
providers.put(e.getKey(), e.getValue().getAsString());
}
}
return new SongResult(title, artist, playlist, providers);
}
}

View File

@@ -0,0 +1,152 @@
package de.oaa.linkster;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import de.oaa.linkster.model.SongResult;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class GameActivity extends AppCompatActivity {
private static final int REQUEST_SCAN = 1;
private Button btnNextCard;
private ProgressBar progressBar;
private StreamingService selectedService;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
btnNextCard = findViewById(R.id.btnNextCard);
progressBar = findViewById(R.id.progressBar);
selectedService = (StreamingService) getIntent().getSerializableExtra("service");
if (selectedService == null) selectedService = StreamingService.DEEZER;
startScanner();
btnNextCard.setOnClickListener(v -> startScanner());
}
private void startScanner() {
startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_SCAN) {
if (resultCode == RESULT_OK && data != null) {
String url = data.getStringExtra(ScanActivity.EXTRA_RESULT);
if (url != null) handleScannedUrl(url);
}
// Abbrechen → einfach auf der GameActivity bleiben
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void handleScannedUrl(String url) {
setLoading(true);
executor.execute(() -> {
try {
SongResult song = ApiClient.getInstance().resolve(url);
if (song == null) {
runOnUiThread(() -> {
setLoading(false);
showErrorDialog("Song nicht gefunden",
"Dieser QR-Code ist keiner bekannten Hitster-Karte zugeordnet.");
});
return;
}
String serviceUrl = song.getUrlFor(selectedService.providerKey);
if (serviceUrl == null) {
runOnUiThread(() -> {
setLoading(false);
showNotAvailableDialog(song.toString());
});
return;
}
runOnUiThread(() -> openInService(song, serviceUrl));
} catch (Exception e) {
runOnUiThread(() -> {
setLoading(false);
showErrorDialog("Fehler", e.getMessage());
});
}
});
}
private void openInService(SongResult song, String url) {
setLoading(false);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setPackage(selectedService.packageName);
try {
startActivity(intent);
if (selectedService.backgroundPlayback) {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
Intent bringBack = new Intent(this, GameActivity.class);
bringBack.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(bringBack);
}, 300);
}
} catch (android.content.ActivityNotFoundException e) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}
private void showNotAvailableDialog(String songName) {
new AlertDialog.Builder(this)
.setTitle("Nicht verfügbar")
.setMessage("\"" + songName + "\"\n\nist auf " + selectedService.displayName
+ " nicht verfügbar.\n\nBitte ziehe die nächste Karte.")
.setPositiveButton("Nächste Karte", (d, w) -> startScanner())
.setNegativeButton("Abbrechen", null)
.show();
}
private void showErrorDialog(String title, String message) {
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setPositiveButton("OK", null)
.show();
}
private void setLoading(boolean loading) {
progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
btnNextCard.setEnabled(!loading);
}
@Override
public void onBackPressed() {
super.onBackPressed();
// finish() → zurück zu MainActivity
}
@Override
protected void onDestroy() {
super.onDestroy();
executor.shutdown();
}
}

View File

@@ -0,0 +1,82 @@
package de.oaa.linkster;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
private static final String PREFS_NAME = "linkster_prefs";
private static final String PREF_SERVICE = "selected_service";
private Button btnStart;
private ProgressBar progressBar;
private TextView tvStatus;
private Spinner spinnerService;
private StreamingService selectedService = StreamingService.DEEZER;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnStart = findViewById(R.id.btnScan);
progressBar = findViewById(R.id.progressBar);
tvStatus = findViewById(R.id.tvStatus);
spinnerService = findViewById(R.id.spinnerService);
setupServiceSpinner();
btnStart.setOnClickListener(v -> startGame());
}
private void setupServiceSpinner() {
StreamingService[] services = StreamingService.values();
ArrayAdapter<StreamingService> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, services);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerService.setAdapter(adapter);
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
int savedIndex = prefs.getInt(PREF_SERVICE, 0);
spinnerService.setSelection(savedIndex);
selectedService = services[savedIndex];
spinnerService.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
selectedService = services[pos];
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
.edit().putInt(PREF_SERVICE, pos).apply();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
}
private void startGame() {
Intent intent = new Intent(this, GameActivity.class);
intent.putExtra("service", selectedService);
startActivity(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
executor.shutdown();
}
}

View File

@@ -0,0 +1,133 @@
package de.oaa.linkster;
import android.content.Context;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.oaa.linkster.model.SongResult;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PlaylistDatabase {
private static PlaylistDatabase instance;
public static PlaylistDatabase getInstance(Context context) {
if (instance == null) instance = new PlaylistDatabase(context);
return instance;
}
private static class PlaylistEntry {
final String id;
final String name;
final Pattern matcher;
PlaylistEntry(String id, String name, Pattern matcher) {
this.id = id;
this.name = name;
this.matcher = matcher;
}
}
private final Context context;
private final List<PlaylistEntry> playlists = new ArrayList<>();
private final Map<String, JsonObject> songCache = new HashMap<>();
public PlaylistDatabase(Context context) {
this.context = context.getApplicationContext();
loadIndex();
}
private void loadIndex() {
try {
JsonElement root = readAsset("playlists/all.json");
for (JsonElement elem : root.getAsJsonArray()) {
JsonObject obj = elem.getAsJsonObject();
if (!obj.has("matcher")) continue;
if (obj.has("available") && !obj.get("available").getAsBoolean()) continue;
String id = obj.get("id").getAsString();
String name = obj.get("name").getAsString();
// Matcher ist im JS-Regex-Format: /pattern$/ → Slashes entfernen
String rawMatcher = obj.get("matcher").getAsString();
String patternStr = rawMatcher.replaceAll("^/|/[a-z]*$", "");
try {
Pattern pattern = Pattern.compile(patternStr);
playlists.add(new PlaylistEntry(id, name, pattern));
} catch (Exception ignored) {}
}
} catch (Exception e) {
throw new RuntimeException("Fehler beim Laden der Playlist-Datenbank: " + e.getMessage(), e);
}
}
/**
* Sucht anhand der gescannten Hitster-URL nach dem Song und gibt den Deezer-Link zurück.
* URL-Format: https://www.hitstergame.com/de/00042 oder ähnlich
*/
public SongResult findSong(String scannedUrl) throws IOException {
for (PlaylistEntry entry : playlists) {
Matcher m = entry.matcher.matcher(scannedUrl);
if (!m.find()) continue;
String songIdStr = m.group(1);
int songId = Integer.parseInt(songIdStr);
JsonObject songs = getSongs(entry.id);
JsonObject song = songs.getAsJsonObject(String.valueOf(songId));
if (song == null) return null;
String title = song.get("title").getAsString();
String artist = song.get("artistName").getAsString();
Map<String, String> providerUrls = extractProviderUrls(song);
return new SongResult(title, artist, entry.name, providerUrls);
}
return null;
}
private Map<String, String> extractProviderUrls(JsonObject song) {
Map<String, String> result = new LinkedHashMap<>();
JsonObject providers = song.getAsJsonObject("providers");
if (providers == null) return result;
for (Map.Entry<String, JsonElement> providerEntry : providers.entrySet()) {
JsonObject regions = providerEntry.getValue().getAsJsonObject();
for (Map.Entry<String, JsonElement> regionEntry : regions.entrySet()) {
String url = regionEntry.getValue().getAsString();
if (url != null && !url.isEmpty()) {
result.put(providerEntry.getKey(), url);
break; // erstes verfügbares Land reicht
}
}
}
return result;
}
private JsonObject getSongs(String playlistId) throws IOException {
if (songCache.containsKey(playlistId)) {
return songCache.get(playlistId);
}
JsonObject playlist = readAsset("playlists/" + playlistId + ".json").getAsJsonObject();
JsonObject songs = playlist.getAsJsonObject("songs");
songCache.put(playlistId, songs);
return songs;
}
private JsonElement readAsset(String path) throws IOException {
try (InputStream is = context.getAssets().open(path);
InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
return JsonParser.parseReader(reader);
}
}
}

View File

@@ -0,0 +1,136 @@
package de.oaa.linkster;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.barcode.BarcodeScanner;
import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
import com.google.mlkit.vision.barcode.BarcodeScanning;
import com.google.mlkit.vision.barcode.common.Barcode;
import com.google.mlkit.vision.common.InputImage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ScanActivity extends AppCompatActivity {
public static final String EXTRA_RESULT = "SCAN_RESULT";
private static final int REQUEST_CAMERA = 1;
private PreviewView previewView;
private ExecutorService cameraExecutor;
private boolean resultDelivered = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scan);
previewView = findViewById(R.id.previewView);
cameraExecutor = Executors.newSingleThreadExecutor();
findViewById(R.id.btnCancel).setOnClickListener(v -> {
setResult(RESULT_CANCELED);
finish();
});
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CAMERA
&& grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
setResult(RESULT_CANCELED);
finish();
}
}
private void startCamera() {
ListenableFuture<ProcessCameraProvider> future =
ProcessCameraProvider.getInstance(this);
future.addListener(() -> {
try {
ProcessCameraProvider cameraProvider = future.get();
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
BarcodeScannerOptions options = new BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build();
BarcodeScanner scanner = BarcodeScanning.getClient(options);
ImageAnalysis analysis = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
analysis.setAnalyzer(cameraExecutor, imageProxy -> {
@SuppressWarnings("UnsafeOptInUsageError")
InputImage image = InputImage.fromMediaImage(
imageProxy.getImage(),
imageProxy.getImageInfo().getRotationDegrees()
);
scanner.process(image)
.addOnSuccessListener(barcodes -> {
if (!resultDelivered && !barcodes.isEmpty()) {
String url = barcodes.get(0).getRawValue();
if (url != null) {
resultDelivered = true;
Intent result = new Intent();
result.putExtra(EXTRA_RESULT, url);
setResult(RESULT_OK, result);
finish();
}
}
})
.addOnCompleteListener(task -> imageProxy.close());
});
cameraProvider.unbindAll();
cameraProvider.bindToLifecycle(this,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis);
} catch (Exception e) {
setResult(RESULT_CANCELED);
finish();
}
}, ContextCompat.getMainExecutor(this));
}
@Override
protected void onDestroy() {
super.onDestroy();
cameraExecutor.shutdown();
}
}

View File

@@ -0,0 +1,31 @@
package de.oaa.linkster;
public enum StreamingService {
// Anzeigename JSON-Key Package Im Hintergrund?
DEEZER ("Deezer", "deezer", "deezer.android.app", true),
SPOTIFY ("Spotify", "spotify", "com.spotify.music", true),
APPLE_MUSIC ("Apple Music", "appleMusic", "com.apple.android.music", true),
AMAZON ("Amazon Music", "amazonMusic", "com.amazon.mp3", true),
TIDAL ("Tidal", "tidal", "com.aspiro.tidal", true),
SOUNDCLOUD ("SoundCloud", "soundcloud", "com.soundcloud.android", true),
YOUTUBE ("YouTube", "youtube", "com.google.android.youtube", false),
YOUTUBE_MUSIC("YouTube Music","youtubeMusic", "com.google.android.apps.youtube.music", false);
public final String displayName;
public final String providerKey;
public final String packageName;
/** Audio-Dienste spielen im Hintergrund App nach dem Öffnen zurückbringen. */
public final boolean backgroundPlayback;
StreamingService(String displayName, String providerKey, String packageName, boolean backgroundPlayback) {
this.displayName = displayName;
this.providerKey = providerKey;
this.packageName = packageName;
this.backgroundPlayback = backgroundPlayback;
}
@Override
public String toString() {
return displayName;
}
}

View File

@@ -0,0 +1,26 @@
package de.oaa.linkster.model;
import java.util.Map;
public class SongResult {
public final String title;
public final String artist;
public final String playlistName;
public final Map<String, String> providerUrls;
public SongResult(String title, String artist, String playlistName, Map<String, String> providerUrls) {
this.title = title;
this.artist = artist;
this.playlistName = playlistName;
this.providerUrls = providerUrls;
}
public String getUrlFor(String providerKey) {
return providerUrls.get(providerKey);
}
@Override
public String toString() {
return artist + " " + title;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#CC0A1A12" />
<corners android:radius="24dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/surface" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="@color/primary" />
</shape>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/background"
android:padding="32dp"
tools:context=".GameActivity">
<ImageView
android:layout_width="160dp"
android:layout_height="160dp"
android:src="@drawable/ic_launcher_foreground"
android:scaleType="fitCenter"
android:layout_marginBottom="48dp"
android:contentDescription="Linkster Logo" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:visibility="gone"
android:indeterminateTint="@color/primary" />
<Button
android:id="@+id/btnNextCard"
android:layout_width="220dp"
android:layout_height="56dp"
android:text="Nächste Karte"
android:textSize="16sp"
android:textStyle="bold"
android:backgroundTint="@color/primary"
android:textColor="@color/white"
android:stateListAnimator="@null" />
</LinearLayout>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/background"
android:padding="32dp"
tools:context=".MainActivity">
<!-- Logo -->
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/ic_launcher_foreground"
android:scaleType="fitCenter"
android:layout_marginBottom="12dp"
android:contentDescription="Linkster Logo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Linkster"
android:textSize="32sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HitStar QR-Code → Streaming"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="40dp" />
<!-- Dienst-Auswahl -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="STREAMING-DIENST"
android:textSize="11sp"
android:letterSpacing="0.12"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp" />
<Spinner
android:id="@+id/spinnerService"
android:layout_width="220dp"
android:layout_height="48dp"
android:layout_marginBottom="40dp"
android:background="@drawable/spinner_background"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:popupBackground="@color/surface" />
<!-- Scan-Button -->
<Button
android:id="@+id/btnScan"
android:layout_width="220dp"
android:layout_height="56dp"
android:text="Spiel starten"
android:textSize="16sp"
android:textStyle="bold"
android:backgroundTint="@color/primary"
android:textColor="@color/white"
android:stateListAnimator="@null" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:visibility="gone"
android:indeterminateTint="@color/primary" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:textSize="13sp"
android:textColor="@color/text_secondary" />
</LinearLayout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Oberer Hinweis -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:layout_marginTop="48dp"
android:text="Karte vor die Kamera halten"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
android:background="@drawable/scan_label_background"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<!-- Abbrechen-Button -->
<Button
android:id="@+id/btnCancel"
android:layout_width="160dp"
android:layout_height="48dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="48dp"
android:text="Abbrechen"
android:textColor="@color/white"
android:backgroundTint="@color/surface"
android:stateListAnimator="@null" />
</FrameLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Logo-Gradient: Dunkelgrün → Teal → Blau -->
<color name="green_dark">#1A6B3A</color>
<color name="teal">#1B7A6B</color>
<color name="blue_dark">#1A4A9B</color>
<!-- App-Farben -->
<color name="primary">#27A882</color> <!-- Helles Teal Buttons, Akzente -->
<color name="primary_dark">#1B7A6B</color> <!-- Dunkleres Teal -->
<color name="background">#0A1A12</color> <!-- Sehr dunkles Grün App-Hintergrund -->
<color name="surface">#122B1E</color> <!-- Etwas heller Cards, Spinner -->
<color name="text_primary">#FFFFFF</color>
<color name="text_secondary">#7FBFA3</color> <!-- Gedämpftes Teal-Weiß -->
<color name="white">#FFFFFF</color>
<color name="ic_launcher_background">#1A6B3A</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Linkster</string>
</resources>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Linkster" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary_dark</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSurface">@color/surface</item>
<item name="colorOnSurface">@color/text_primary</item>
<item name="android:windowBackground">@color/background</item>
<item name="android:statusBarColor">@color/background</item>
<item name="android:navigationBarColor">@color/background</item>
<!-- Spinner-Styling -->
<item name="colorControlNormal">@color/primary</item>
<item name="colorControlActivated">@color/primary</item>
<item name="android:textColorPrimary">@color/text_primary</item>
<item name="android:textColorSecondary">@color/text_secondary</item>
</style>
</resources>