Initialier Commit
50
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
61
app/src/main/java/de/oaa/linkster/ApiClient.java
Normal 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);
|
||||
}
|
||||
}
|
||||
152
app/src/main/java/de/oaa/linkster/GameActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
82
app/src/main/java/de/oaa/linkster/MainActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
133
app/src/main/java/de/oaa/linkster/PlaylistDatabase.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
136
app/src/main/java/de/oaa/linkster/ScanActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/de/oaa/linkster/StreamingService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
26
app/src/main/java/de/oaa/linkster/model/SongResult.java
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
6
app/src/main/res/drawable/scan_label_background.xml
Normal 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>
|
||||
7
app/src/main/res/drawable/spinner_background.xml
Normal 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>
|
||||
39
app/src/main/res/layout/activity_game.xml
Normal 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>
|
||||
87
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
40
app/src/main/res/layout/activity_scan.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal 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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
17
app/src/main/res/values/colors.xml
Normal 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>
|
||||
4
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Linkster</string>
|
||||
</resources>
|
||||
19
app/src/main/res/values/themes.xml
Normal 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>
|
||||