mirror of
https://github.com/Michatec/Radio.git
synced 2026-05-31 05:32:39 +02:00
Compare commits
47 Commits
9d47684f13
...
14.5
| Author | SHA1 | Date | |
|---|---|---|---|
| f6208f5e5a | |||
| a1beb17b26 | |||
| 18c28170c5 | |||
| 48836334bb | |||
| 133c56be4d | |||
| 63d85118a4 | |||
| 4f150221b7 | |||
| 5fbf763bd6 | |||
| 0d3224214e | |||
| 11610084f9 | |||
| 08f303997e | |||
| b60b8cdb7c | |||
| b3de68050c | |||
| 5d99b2a113 | |||
| adac340925 | |||
| d6037dc0c2 | |||
| 297310a784 | |||
| abd9b5ecd9 | |||
| 744a650a91 | |||
| 1238a5fba2 | |||
| cc0b284a7f | |||
| dfbbd8da33 | |||
| 7690ce5685 | |||
| 3bbc9280b5 | |||
| 0fe32420f2 | |||
| 1491a084f3 | |||
| 244121edb1 | |||
| 5479ae25f2 | |||
| c0ef50b5a9 | |||
| 5fb775a373 | |||
| 901ae6b8ad | |||
| 255f27bddf | |||
| 45576e1577 | |||
| 1212fc61b5 | |||
| cdf7668d43 | |||
| f755dc5173 | |||
| 8c7a8ce7c4 | |||
| d1cc340417 | |||
| e0d1770a19 | |||
| 2e8cc9b243 | |||
| 9e1219549e | |||
| 17ba1c268a | |||
| b328af5c3a | |||
| d31d476cb5 | |||
| 6bb34cd707 | |||
| 883a4443e9 | |||
| 8ba22a4c09 |
@@ -7,11 +7,14 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ANDROID_HOME: /usr/local/lib/android/sdk/
|
ANDROID_HOME: /usr/local/lib/android/sdk/
|
||||||
APK_PATH: app/build/outputs/apk/release/Radio.apk
|
APK_PATH: app/build/outputs/apk/release/app-release-unsigned.apk
|
||||||
APKSIGNER: /usr/local/lib/android/sdk/build-tools/34.0.0/apksigner
|
APKSIGNER: /usr/local/lib/android/sdk/build-tools/34.0.0/apksigner
|
||||||
|
ZIPALIGN: /usr/local/lib/android/sdk/build-tools/34.0.0/zipalign
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -29,20 +32,24 @@ jobs:
|
|||||||
distribution: adopt
|
distribution: adopt
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Extract Build Metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
# Extract Version Name
|
||||||
|
VERSION_NAME=$(grep "versionName =" app/build.gradle.kts | awk -F\" '{print $2}')
|
||||||
|
echo "Found Version Name: $VERSION_NAME"
|
||||||
|
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Android SDK
|
- name: Cache Android SDK
|
||||||
#id: cache-android-sdk
|
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.ANDROID_HOME }}
|
path: ${{ env.ANDROID_HOME }}
|
||||||
key: ${{ runner.os }}-android-sdk
|
key: ${{ runner.os }}-android-sdk
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
## It is not necessary to check for cache hit as it
|
|
||||||
## will not download Android SDK again
|
|
||||||
#if: steps.cache-android-sdk.outputs.cache-hit != 'true'
|
|
||||||
uses: android-actions/setup-android@v4
|
uses: android-actions/setup-android@v4
|
||||||
with:
|
with:
|
||||||
packages: ''
|
packages: 'ndk;29.0.14206865'
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x ./gradlew
|
run: chmod +x ./gradlew
|
||||||
@@ -55,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Zipalign APK
|
- name: Zipalign APK
|
||||||
run: |
|
run: |
|
||||||
/usr/local/lib/android/sdk/build-tools/34.0.0/zipalign -v -p 4 ${{ env.APK_PATH }} app-release-aligned.apk
|
${{ env.ZIPALIGN }} -v -p 4 ${{ env.APK_PATH }} app-release-aligned.apk
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
env:
|
env:
|
||||||
@@ -66,10 +73,10 @@ jobs:
|
|||||||
echo "$SIGN_KEY" | base64 -d > key.der
|
echo "$SIGN_KEY" | base64 -d > key.der
|
||||||
${{ env.APKSIGNER }} sign --key key.der --cert cert.der app-release-aligned.apk
|
${{ env.APKSIGNER }} sign --key key.der --cert cert.der app-release-aligned.apk
|
||||||
rm cert.der key.der
|
rm cert.der key.der
|
||||||
mv app-release-aligned.apk app-release.apk
|
mv app-release-aligned.apk Radio_${{ env.VERSION_NAME }}-release.apk
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: app-release
|
name: Radio
|
||||||
path: app-release.apk
|
path: Radio_${{ env.VERSION_NAME }}-release.apk
|
||||||
|
|||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Android",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/**"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"_DEBUG",
|
||||||
|
"UNICODE",
|
||||||
|
"_UNICODE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"C_Cpp.default.compilerPath": "",
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive"
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
### ℹ️ About Radio
|
### ℹ️ About Radio
|
||||||
**Radio is an application with a minimalist approach to listening to radio over the Internet.** <br>
|
**Radio is an application with a minimalist approach to listening to radio over the Internet.** <br>
|
||||||
**Radio only offers a very basic search option, and it imports audio streaming links when you tap them in a web browser.** <br>
|
**Radio only offers a very basic search option, and it imports audio streaming links when you tap them in a web browser.** <br>
|
||||||
**Radio now also supports Android TV (Beta).** <br>
|
**Radio now also supports Android TV (Beta) and Cast to Devices (Beta) with the Version 14.5 and above.** <br>
|
||||||
**Pull request are welcome at any time.**<br>
|
**Pull request are welcome at any time.**<br>
|
||||||
|
|
||||||
**Radio is free software. It is released under the [GPLv3 open source license](https://opensource.org/licenses/GPL-3.0).**
|
**Radio is free software. It is released under the [GPLv3 open source license](https://opensource.org/licenses/GPL-3.0).**
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
We take the security of Radio seriously. If you believe you have found a security vulnerability, please report it via **GitHub Issues** before disclosing it publicly.
|
||||||
|
|
||||||
|
**Create a new security issue**: [GitHub Issues — Security](https://github.com/michatec/Radio/issues/new?labels=security). We will respond within 48 hours acknowledging your report and work with you to understand and address the issue.
|
||||||
|
|
||||||
|
Include in your report:
|
||||||
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce
|
||||||
|
- Affected versions (if known)
|
||||||
|
- Potential impact
|
||||||
|
- Any suggested fixes (optional)
|
||||||
|
|
||||||
|
We appreciate responsible disclosure and will credit researchers who report valid security issues (unless you prefer to remain anonymous).
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 14.5 | :white_check_mark: |
|
||||||
|
| < 14.5 | :x: |
|
||||||
|
|
||||||
|
Only the latest stable release receives security updates. Users on older versions are encouraged to update.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
Radio streams audio over the internet and connects to the [radio-browser.info](https://www.radio-browser.info/) API for station search. The app allows cleartext HTTP traffic for radio stream compatibility (required for many legacy radio stations).
|
||||||
|
|
||||||
|
### Data Collection
|
||||||
|
- **Station data** (names, images, stream URLs) is fetched from radio-browser.info's public API
|
||||||
|
- **Station lists** are stored locally on the device only
|
||||||
|
- **No personal data** is collected or transmitted to michatec servers
|
||||||
|
- **Usage data** is not collected
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
Radio requests only the permissions necessary for core functionality:
|
||||||
|
- `INTERNET` — stream radio and fetch station metadata
|
||||||
|
- `ACCESS_NETWORK_STATE` — detect connectivity changes
|
||||||
|
- `FOREGROUND_SERVICE_MEDIA_PLAYBACK` — maintain playback when app is backgrounded
|
||||||
|
- `WAKE_LOCK` — prevent device from sleeping during playback
|
||||||
|
|
||||||
|
### Third-Party Dependencies
|
||||||
|
Radio uses several third-party libraries. Security issues in dependencies are monitored via Renovate bot for updates. Key dependencies include:
|
||||||
|
- AndroidX Media3 / ExoPlayer (media playback)
|
||||||
|
- Google Cast SDK (Chromecast support)
|
||||||
|
- Volley (HTTP requests)
|
||||||
|
|
||||||
|
### File Handling
|
||||||
|
The app can import M3U/PLS playlist files from external sources. Files are processed locally and stream URLs are validated before playback. Station images are downloaded from radio-browser.info and cached locally.
|
||||||
|
|
||||||
|
## Security Update Process
|
||||||
|
Security patches are delivered via normal app update channels (GitHub Releases, automated update notifications). Critical vulnerabilities may trigger an out-of-band security update.
|
||||||
|
|
||||||
|
## Contacts
|
||||||
|
- General issues: [GitHub Issues](https://github.com/michatec/Radio/issues)
|
||||||
|
- Project maintainer: [@michatec](https://github.com/michatec)
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias libs.plugins.android.application
|
|
||||||
id 'kotlin-parcelize'
|
|
||||||
}
|
|
||||||
|
|
||||||
androidComponents {
|
|
||||||
onVariants(selector().all()) { variant ->
|
|
||||||
variant.outputs.forEach { output ->
|
|
||||||
output.outputFileName.set("Radio.apk")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'com.michatec.radio'
|
|
||||||
compileSdk 36
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId 'com.michatec.radio'
|
|
||||||
minSdk 28
|
|
||||||
targetSdk 36
|
|
||||||
versionCode 145
|
|
||||||
versionName '14.5'
|
|
||||||
resourceConfigurations += ['en', 'de', 'el', 'nl', 'pl', 'ru','uk', 'ja', 'da', 'fr']
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
cppFlags ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
buildConfig true
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
minifyEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
crunchPngs false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
applicationIdSuffix = ".debug"
|
|
||||||
}
|
|
||||||
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
crunchPngs true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path file('src/main/cpp/CMakeLists.txt')
|
|
||||||
version '3.22.1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
|
|
||||||
// Google Stuff //
|
|
||||||
implementation libs.material
|
|
||||||
implementation libs.gson
|
|
||||||
implementation libs.play.services.cast.framework
|
|
||||||
|
|
||||||
// AndroidX Stuff //
|
|
||||||
implementation libs.core.ktx
|
|
||||||
implementation libs.activity.ktx
|
|
||||||
implementation libs.palette.ktx
|
|
||||||
implementation libs.preference.ktx
|
|
||||||
implementation libs.media
|
|
||||||
implementation libs.media3.exoplayer
|
|
||||||
implementation libs.media3.exoplayer.hls
|
|
||||||
implementation libs.media3.session
|
|
||||||
implementation libs.media3.cast
|
|
||||||
implementation libs.media3.datasource.okhttp
|
|
||||||
implementation libs.navigation.fragment.ktx
|
|
||||||
implementation libs.navigation.ui.ktx
|
|
||||||
implementation libs.work.runtime.ktx
|
|
||||||
implementation libs.leanback
|
|
||||||
|
|
||||||
implementation libs.freedroidwarn
|
|
||||||
|
|
||||||
// Volley HTTP request //
|
|
||||||
implementation libs.volley
|
|
||||||
implementation libs.material3
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.michatec.radio"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.michatec.radio"
|
||||||
|
minSdk = 28
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 145
|
||||||
|
versionName = "14.5"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
isCrunchPngs = false
|
||||||
|
proguardFiles.addAll(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), file("proguard-rules.pro")))
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
}
|
||||||
|
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
isCrunchPngs = true
|
||||||
|
proguardFiles.addAll(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), file("proguard-rules.pro")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ndkVersion = "29.0.14206865"
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
|
version = "3.22.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Google Stuff //
|
||||||
|
implementation(libs.material)
|
||||||
|
implementation(libs.gson)
|
||||||
|
implementation(libs.play.services.cast.framework)
|
||||||
|
|
||||||
|
// AndroidX Stuff //
|
||||||
|
implementation(libs.core.ktx)
|
||||||
|
implementation(libs.activity.ktx)
|
||||||
|
implementation(libs.palette.ktx)
|
||||||
|
implementation(libs.preference.ktx)
|
||||||
|
implementation(libs.media)
|
||||||
|
implementation(libs.media3.exoplayer)
|
||||||
|
implementation(libs.media3.exoplayer.hls)
|
||||||
|
implementation(libs.media3.session)
|
||||||
|
implementation(libs.media3.cast)
|
||||||
|
implementation(libs.media3.datasource.okhttp)
|
||||||
|
implementation(libs.navigation.fragment.ktx)
|
||||||
|
implementation(libs.navigation.ui.ktx)
|
||||||
|
implementation(libs.work.runtime.ktx)
|
||||||
|
implementation(libs.leanback)
|
||||||
|
|
||||||
|
implementation(libs.freedroidwarn)
|
||||||
|
|
||||||
|
// Volley HTTP request //
|
||||||
|
implementation(libs.volley)
|
||||||
|
implementation(libs.material3)
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# You can control the set of applied configuration files using the
|
||||||
# proguardFiles setting in build.gradle.
|
# proguardFiles setting in build.gradle.kts.
|
||||||
#
|
#
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".Radio"
|
android:name=".Radio"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@mipmap/ic_launcher"
|
android:banner="@mipmap/ic_banner"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|||||||
+293
-122
@@ -3,6 +3,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <complex>
|
#include <complex>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#if defined(__ARM_NEON)
|
#if defined(__ARM_NEON)
|
||||||
#include <arm_neon.h>
|
#include <arm_neon.h>
|
||||||
@@ -12,72 +13,81 @@
|
|||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static constexpr int FFT_SIZE = 512;
|
static std::atomic<float> gSampleRate(44100.0f);
|
||||||
|
static constexpr int FFT_SIZE = 2048;
|
||||||
static constexpr int NUM_EQ_BANDS = 10;
|
static constexpr int NUM_EQ_BANDS = 10;
|
||||||
static constexpr float INV_32768 = 1.0f / 32768.0f;
|
static constexpr float INV_32768 = 1.0f / 32768.0f;
|
||||||
static constexpr float SQRT_2_INV = 0.70710678f;
|
static constexpr float SQRT_2_INV = 0.70710678f;
|
||||||
static constexpr float DENORMAL_OFFSET = 1e-18f;
|
static constexpr float DENORMAL_OFFSET = 1e-18f;
|
||||||
|
static constexpr float INTERPOLATION_SPEED = 0.5f;
|
||||||
|
|
||||||
static constexpr std::array<float, NUM_EQ_BANDS> EQ_FREQUENCIES = {
|
static constexpr std::array<float, NUM_EQ_BANDS> EQ_FREQUENCIES = {
|
||||||
31.25f, 62.5f, 125.0f, 250.0f, 500.0f,
|
31.25f, 62.5f, 125.0f, 250.0f, 500.0f,
|
||||||
1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f
|
1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f
|
||||||
};
|
};
|
||||||
|
|
||||||
struct alignas(16) BiquadBank {
|
struct alignas(16) EqBandInterpolator {
|
||||||
alignas(16) std::array<float, NUM_EQ_BANDS> a0{}, a1{}, a2{}, b1{}, b2{};
|
std::atomic<float> targetGain{0.0f};
|
||||||
alignas(16) std::array<float, NUM_EQ_BANDS> z1{}, z2{};
|
std::atomic<float> currentGain{0.0f};
|
||||||
uint16_t activeMask = 0;
|
float a0 = 1.0f, a1 = 0.0f, a2 = 0.0f, b1 = 0.0f, b2 = 0.0f;
|
||||||
|
float z1 = 0.0f, z2 = 0.0f;
|
||||||
|
bool active = false;
|
||||||
|
|
||||||
[[nodiscard]] inline bool hasActiveBands() const { return activeMask != 0; }
|
inline void setTargetGain(float g) { targetGain.store(g, std::memory_order_release); }
|
||||||
|
|
||||||
inline void setBandActive(int band, bool active) {
|
inline void updateInterpolation() {
|
||||||
if (active) activeMask |= (1 << band);
|
float target = targetGain.load(std::memory_order_acquire);
|
||||||
else activeMask &= ~(1 << band);
|
float current = currentGain.load(std::memory_order_relaxed);
|
||||||
}
|
float diff = target - current;
|
||||||
|
if (std::abs(diff) > 0.001f) {
|
||||||
inline void processBlock(float* __restrict__ data, int count) {
|
currentGain.store(current + diff * INTERPOLATION_SPEED, std::memory_order_release);
|
||||||
if (!this -> hasActiveBands()) return;
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
float x = data[i];
|
|
||||||
#pragma GCC unroll 10
|
|
||||||
for (int b = 0; b < NUM_EQ_BANDS; b++) {
|
|
||||||
if (activeMask & (1 << b)) {
|
|
||||||
float y = x * a0[b] + z1[b];
|
|
||||||
z1[b] = x * a1[b] + z2[b] - b1[b] * y + DENORMAL_OFFSET;
|
|
||||||
z2[b] = x * a2[b] - b2[b] * y;
|
|
||||||
x = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data[i] = x;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPeakingEQ(int band, float sr, float f, float g, float bw) {
|
inline float process(float x) {
|
||||||
if (band < 0 || band >= NUM_EQ_BANDS) return;
|
if (!active) return x;
|
||||||
const bool active = std::abs(g) > 0.1f;
|
updateInterpolation();
|
||||||
setBandActive(band, active);
|
float y = x * a0 + z1;
|
||||||
if (!active) return;
|
z1 = x * a1 + z2 - b1 * y + DENORMAL_OFFSET;
|
||||||
const float A = powf(10.0f, g / 40.0f);
|
z2 = x * a2 - b2 * y;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void setCoefficients(float sr, float f, float g, float bw) {
|
||||||
|
active = true;
|
||||||
|
const float A = powf(10.0f, g / 60.0f);
|
||||||
const float w = 2.0f * static_cast<float>(M_PI) * f / sr;
|
const float w = 2.0f * static_cast<float>(M_PI) * f / sr;
|
||||||
const float alpha = sinf(w) * sinhf(logf(2.0f) / 2.0f * bw * w / sinf(w));
|
const float alpha = sinf(w) * sinhf(logf(2.0f) / 2.0f * bw * w / sinf(w));
|
||||||
const float c = cosf(w);
|
const float c = cosf(w);
|
||||||
const float a0_raw = 1.0f + alpha / A;
|
const float a0_raw = 1.0f + alpha / A;
|
||||||
const float invA0 = 1.0f / a0_raw;
|
const float invA0 = 1.0f / a0_raw;
|
||||||
a0[band] = (1.0f + alpha * A) * invA0;
|
a0 = (1.0f + alpha * A) * invA0;
|
||||||
a1[band] = (-2.0f * c) * invA0;
|
a1 = (-2.0f * c) * invA0;
|
||||||
a2[band] = (1.0f - alpha * A) * invA0;
|
a2 = (1.0f - alpha * A) * invA0;
|
||||||
b1[band] = (-2.0f * c) * invA0;
|
b1 = (-2.0f * c) * invA0;
|
||||||
b2[band] = (1.0f - alpha / A) * invA0;
|
b2 = (1.0f - alpha / A) * invA0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct alignas(16) BassFilter {
|
struct alignas(16) BassFilter {
|
||||||
alignas(16) float a0 = 1.2f, a1 = 1.2f, a2 = 1.2f, b1 = 0.0f, b2 = 0.0f;
|
alignas(16) float a0 = 1.2f, a1 = 1.2f, a2 = 1.2f, b1 = 0.0f, b2 = 0.0f;
|
||||||
alignas(16) float z1 = 0.0f, z2 = 0.0f;
|
alignas(16) float z1 = 0.0f, z2 = 0.0f;
|
||||||
bool active = false;
|
std::atomic<bool> active{false};
|
||||||
|
std::atomic<float> targetGain{0.0f};
|
||||||
|
std::atomic<float> currentGain{0.0f};
|
||||||
|
|
||||||
|
inline void updateInterpolation() {
|
||||||
|
float target = targetGain.load(std::memory_order_acquire);
|
||||||
|
float current = currentGain.load(std::memory_order_relaxed);
|
||||||
|
float diff = target - current;
|
||||||
|
if (std::abs(diff) > 0.001f) {
|
||||||
|
currentGain.store(current + diff * INTERPOLATION_SPEED, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline float process(float x) {
|
inline float process(float x) {
|
||||||
if (!active) return x;
|
if (!active.load(std::memory_order_acquire)) return x;
|
||||||
|
updateInterpolation();
|
||||||
float y = x * a0 + z1;
|
float y = x * a0 + z1;
|
||||||
z1 = x * a1 + z2 - b1 * y + DENORMAL_OFFSET;
|
z1 = x * a1 + z2 - b1 * y + DENORMAL_OFFSET;
|
||||||
z2 = x * a2 - b2 * y;
|
z2 = x * a2 - b2 * y;
|
||||||
@@ -85,10 +95,8 @@ struct alignas(16) BassFilter {
|
|||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLowShelf(float sr,float f,float g,float q){
|
void setCoefficients(float sr, float f, float g, float q){
|
||||||
active=std::abs(g)>0.01f;
|
float A=powf(10.0f,g/60.0f);
|
||||||
if(!active) return;
|
|
||||||
float A=powf(10.0f,g/40.0f);
|
|
||||||
float w=2.0f*static_cast<float>(M_PI)*f/sr;
|
float w=2.0f*static_cast<float>(M_PI)*f/sr;
|
||||||
float alpha=sinf(w)/2.0f*sqrtf((A+1.0f/A)*(1.0f/q-1.0f)+2.0f);
|
float alpha=sinf(w)/2.0f*sqrtf((A+1.0f/A)*(1.0f/q-1.0f)+2.0f);
|
||||||
float c=cosf(w),sqrtA=sqrtf(A);
|
float c=cosf(w),sqrtA=sqrtf(A);
|
||||||
@@ -100,91 +108,139 @@ struct alignas(16) BassFilter {
|
|||||||
b1=-2.0f*((A-1.0f)+(A+1.0f)*c)*invA0;
|
b1=-2.0f*((A-1.0f)+(A+1.0f)*c)*invA0;
|
||||||
b2=((A+1.0f)+(A-1.0f)*c-2.0f*sqrtA*alpha)*invA0;
|
b2=((A+1.0f)+(A-1.0f)*c-2.0f*sqrtA*alpha)*invA0;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
template<int SIZE>
|
void applyGain(float sr) {
|
||||||
struct CircularBuffer {
|
float g = currentGain.load(std::memory_order_acquire);
|
||||||
alignas(16) std::array<float, SIZE> data = {};
|
setCoefficients(sr, 150.0f, g, SQRT_2_INV);
|
||||||
int pos = 0;
|
}
|
||||||
[[nodiscard]] inline float read() const { return data[pos]; }
|
|
||||||
inline void write(float v) { data[pos] = v; }
|
|
||||||
inline void advance() { pos = (pos + 1) % SIZE; }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class ReverbOptimized {
|
class ReverbOptimized {
|
||||||
std::array<CircularBuffer<1116>, 4> combs;
|
struct DelayLine {
|
||||||
std::array<CircularBuffer<556>, 2> allpasses;
|
float buffer[48000]{};
|
||||||
std::array<float, 4> combFeedback = {0.841f, 0.815f, 0.796f, 0.771f};
|
int size = 48000;
|
||||||
float mix = 0.0f;
|
int pos = 0;
|
||||||
|
|
||||||
|
inline float read(float delaySamples) {
|
||||||
|
float readPos = static_cast<float>(pos) - delaySamples;
|
||||||
|
if (readPos < 0.0f) readPos += static_cast<float>(size);
|
||||||
|
|
||||||
|
int i1 = static_cast<int>(readPos);
|
||||||
|
int i2 = (i1 + 1) % size;
|
||||||
|
float frac = readPos - static_cast<float>(i1);
|
||||||
|
|
||||||
|
return buffer[i1] * (1.0f - frac) + buffer[i2] * frac;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void write(float x) {
|
||||||
|
buffer[pos] = x;
|
||||||
|
pos++;
|
||||||
|
if (pos >= size) pos = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DelayLine delays[8];
|
||||||
|
|
||||||
|
float feedback[8] = {
|
||||||
|
0.78f, 0.80f, 0.82f, 0.84f,
|
||||||
|
0.76f, 0.79f, 0.81f, 0.83f
|
||||||
|
};
|
||||||
|
|
||||||
|
float baseDelay[8] = {
|
||||||
|
1423.0f, 1557.0f, 1617.0f, 1789.0f,
|
||||||
|
1867.0f, 1999.0f, 2137.0f, 2251.0f
|
||||||
|
};
|
||||||
|
|
||||||
|
float modPhase[8] = {};
|
||||||
|
float modSpeed[8] = {
|
||||||
|
0.10f, 0.12f, 0.09f, 0.11f,
|
||||||
|
0.13f, 0.08f, 0.14f, 0.07f
|
||||||
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
inline void setMix(float m) { mix = m; }
|
std::atomic<float> mix{0.0f};
|
||||||
inline float process(float x) {
|
|
||||||
if (mix < 0.01f) return x;
|
inline float processSample(float x) {
|
||||||
|
float m = mix.load(std::memory_order_relaxed);
|
||||||
|
if (m < 0.01f) return x;
|
||||||
float out = 0.0f;
|
float out = 0.0f;
|
||||||
#pragma GCC unroll 4
|
|
||||||
for (int i = 0; i < 4; i++) {
|
#pragma GCC unroll 8
|
||||||
float delayed = combs[i].read();
|
for (int i = 0; i < 8; i++) {
|
||||||
|
modPhase[i] += modSpeed[i];
|
||||||
|
if (modPhase[i] > 2.0f * static_cast<float>(M_PI)) modPhase[i] -= 2.0f * static_cast<float>(M_PI);
|
||||||
|
|
||||||
|
float mod = sinf(modPhase[i]) * 5.0f;
|
||||||
|
|
||||||
|
float delayTime = baseDelay[i] + mod;
|
||||||
|
|
||||||
|
float delayed = delays[i].read(delayTime);
|
||||||
|
|
||||||
|
float input = x + delayed * feedback[i] + DENORMAL_OFFSET;
|
||||||
|
|
||||||
|
delays[i].write(input);
|
||||||
|
|
||||||
out += delayed;
|
out += delayed;
|
||||||
combs[i].write(x + delayed * combFeedback[i] + DENORMAL_OFFSET);
|
|
||||||
combs[i].advance();
|
|
||||||
}
|
}
|
||||||
out *= 0.25f;
|
|
||||||
for (int i = 0; i < 2; i++) {
|
return x * (1.0f - m) + (out * 0.125f) * m;
|
||||||
float bufOut = allpasses[i].read();
|
|
||||||
float xOut = -0.5f * out + bufOut;
|
|
||||||
allpasses[i].write(out + 0.5f * bufOut);
|
|
||||||
allpasses[i].advance();
|
|
||||||
out = xOut;
|
|
||||||
}
|
|
||||||
return x * (1.0f - mix) + out * mix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) {
|
inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) {
|
||||||
if (mix < 0.01f) return;
|
float m = mix.load(std::memory_order_relaxed);
|
||||||
|
if (m < 0.01f) return;
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
left[i] = process(left[i]);
|
float l = processSample(left[i]);
|
||||||
right[i] = process(right[i]);
|
float r = processSample(right[i]);
|
||||||
|
|
||||||
|
float wetL = l * 0.7f + r * 0.3f;
|
||||||
|
float wetR = r * 0.7f + l * 0.3f;
|
||||||
|
|
||||||
|
left[i] = wetL;
|
||||||
|
right[i] = wetR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CompressorOptimized {
|
class CompressorOptimized {
|
||||||
public:
|
public:
|
||||||
float threshold = 0.3f, ratio = 4.0f, attack = 0.08f, release = 0.8f, sampleRate = 44100.0f;
|
std::atomic<float> threshold{0.3f}, ratio{4.0f}, attack{0.08f}, release{0.8f};
|
||||||
|
std::atomic<float> sampleRate{44100.0f};
|
||||||
|
std::atomic<bool> enabled{false};
|
||||||
private:
|
private:
|
||||||
float envelopeL = 0.0f, envelopeR = 0.0f;
|
float envelopeL = 0.0f, envelopeR = 0.0f;
|
||||||
float attackCoef = 0.0f, releaseCoef = 0.0f;
|
float attackCoef = 0.0f, releaseCoef = 0.0f;
|
||||||
bool coefficientsValid = false;
|
|
||||||
public:
|
public:
|
||||||
inline void updateCoefficients() {
|
inline void updateCoefficients() {
|
||||||
if (coefficientsValid) return;
|
float a = attack.load(std::memory_order_relaxed);
|
||||||
attackCoef = expf(-1.0f / (attack * sampleRate));
|
float r = release.load(std::memory_order_relaxed);
|
||||||
releaseCoef = expf(-1.0f / (release * sampleRate));
|
float sr = sampleRate.load(std::memory_order_relaxed);
|
||||||
coefficientsValid = true;
|
|
||||||
|
attackCoef = expf(-1.0f / (a * sr));
|
||||||
|
releaseCoef = expf(-1.0f / (r * sr));
|
||||||
}
|
}
|
||||||
inline void processBlock(float* __restrict__ buffer, int count, float& envelope) {
|
inline void processBlock(float* __restrict__ buffer, int count, float& envelope) {
|
||||||
updateCoefficients();
|
updateCoefficients();
|
||||||
|
float th = threshold.load(std::memory_order_acquire);
|
||||||
|
float rt = ratio.load(std::memory_order_acquire);
|
||||||
for(int i=0; i<count; i++){
|
for(int i=0; i<count; i++){
|
||||||
float absInput = fabsf(buffer[i]);
|
float absInput = fabsf(buffer[i]);
|
||||||
envelope = (absInput > envelope) ? attackCoef*envelope + (1-attackCoef)*absInput : releaseCoef*envelope + (1-releaseCoef)*absInput;
|
envelope = (absInput > envelope) ? attackCoef*envelope + (1.0f-attackCoef)*absInput : releaseCoef*envelope + (1.0f-releaseCoef)*absInput;
|
||||||
float gain = (envelope>threshold)? (threshold + (envelope-threshold)/ratio)/(envelope+1e-9f) : 1.0f;
|
float gain = (envelope>th)? (th + (envelope-th)/rt)/(envelope+1e-9f) : 1.0f;
|
||||||
buffer[i]*=gain;
|
buffer[i]*=gain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inline void process(float* __restrict__ left, float* __restrict__ right, int count) {
|
inline void process(float* __restrict__ left, float* __restrict__ right, int count) {
|
||||||
|
if (!enabled.load(std::memory_order_acquire)) return;
|
||||||
processBlock(left, count, envelopeL);
|
processBlock(left, count, envelopeL);
|
||||||
processBlock(right, count, envelopeR);
|
processBlock(right, count, envelopeR);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
CompressorOptimized gCompressor;
|
static std::atomic<float> gStereoWidth{1.0f};
|
||||||
ReverbOptimized gReverbL, gReverbR;
|
|
||||||
BiquadBank gEqL, gEqR;
|
|
||||||
BassFilter gBassL, gBassR;
|
|
||||||
bool gDrcEnabled = false, gEqEnabled = false, gBassBoostEnabled = false;
|
|
||||||
float gStereoWidth = 1.0f;
|
|
||||||
alignas(16) std::array<float, 4096> gLeftBuf, gRightBuf;
|
alignas(16) std::array<float, 4096> gLeftBuf, gRightBuf;
|
||||||
alignas(16) std::array<float, 256> gFFTData;
|
alignas(16) std::array<float, 256> gFFTData;
|
||||||
alignas(16) std::array<std::complex<float>, FFT_SIZE> gFFTWork;
|
|
||||||
|
|
||||||
inline void fastFFT(std::complex<float>* __restrict__ data, int n) {
|
inline void fastFFT(std::complex<float>* __restrict__ data, int n) {
|
||||||
for (int i = 1, j = 0; i < n; i++) {
|
for (int i = 1, j = 0; i < n; i++) {
|
||||||
@@ -209,32 +265,90 @@ inline void fastFFT(std::complex<float>* __restrict__ data, int n) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void applyHannWindowToReal(std::complex<float>* __restrict__ data, int size) {
|
||||||
|
const auto fSizeMinus1 = static_cast<float>(size - 1);
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
float window = 0.5f * (1.0f - cosf(2.0f * static_cast<float>(M_PI) * static_cast<float>(i) / fSizeMinus1));
|
||||||
|
data[i] = std::complex<float>(data[i].real() * window, data[i].imag());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline float fastSoftClip(float x) {
|
inline float fastSoftClip(float x) {
|
||||||
float ax = fabsf(x);
|
float ax = fabsf(x);
|
||||||
float sign = x > 0 ? 1.0f : -1.0f;
|
float sign = x > 0.0f ? 1.0f : -1.0f;
|
||||||
if (ax > 1.0f) return sign;
|
if (ax > 1.0f) return sign;
|
||||||
return x * (1.5f - 0.5f * x * x);
|
return x * (1.5f - 0.5f * x * x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static EqBandInterpolator gEqL[NUM_EQ_BANDS];
|
||||||
|
static EqBandInterpolator gEqR[NUM_EQ_BANDS];
|
||||||
|
static BassFilter gBassL, gBassR;
|
||||||
|
static CompressorOptimized gCompressor;
|
||||||
|
static ReverbOptimized gReverbL, gReverbR;
|
||||||
|
static std::array<std::complex<float>, FFT_SIZE> gFFTWork;
|
||||||
|
static int gEqUpdateCounter = 0;
|
||||||
|
|
||||||
|
inline void updateAllEqBands() {
|
||||||
|
float sr = gSampleRate.load(std::memory_order_acquire);
|
||||||
|
for (int b = 0; b < NUM_EQ_BANDS; b++) {
|
||||||
|
float g = gEqL[b].targetGain.load(std::memory_order_acquire);
|
||||||
|
gEqL[b].setCoefficients(sr, EQ_FREQUENCIES[static_cast<size_t>(b)], g, 1.0f);
|
||||||
|
gEqR[b].setCoefficients(sr, EQ_FREQUENCIES[static_cast<size_t>(b)], g, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setDrcEnabled(JNIEnv*, jobject, jboolean e) { gDrcEnabled = e; }
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setSampleRate(JNIEnv*, jobject, jfloat sr) {
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setReverbMix(JNIEnv*, jobject, jfloat m) { gReverbL.setMix(m); gReverbR.setMix(m); }
|
gSampleRate.store(sr, std::memory_order_release);
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqBand(JNIEnv*, jobject, jint b, jfloat g) {
|
gCompressor.sampleRate.store(sr, std::memory_order_release);
|
||||||
if (b >= 0 && b < NUM_EQ_BANDS) {
|
gBassL.applyGain(sr);
|
||||||
gEqL.setPeakingEQ(b, 44100.0f, EQ_FREQUENCIES[b], g, 1.0f);
|
gBassR.applyGain(sr);
|
||||||
gEqR.setPeakingEQ(b, 44100.0f, EQ_FREQUENCIES[b], g, 1.0f);
|
gEqUpdateCounter = 1;
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setDrcEnabled(JNIEnv*, jobject, jboolean e) {
|
||||||
|
gCompressor.enabled.store(e == JNI_TRUE, std::memory_order_release);
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setReverbMix(JNIEnv*, jobject, jfloat m) {
|
||||||
|
gReverbL.mix.store(m, std::memory_order_release);
|
||||||
|
gReverbR.mix.store(m, std::memory_order_release);
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqFull(JNIEnv* env, jobject thiz, jfloatArray gains) {
|
||||||
|
if (!gains) return;
|
||||||
|
|
||||||
|
jsize len = env->GetArrayLength(gains);
|
||||||
|
int bandsToUpdate = std::min(static_cast<int>(len), NUM_EQ_BANDS);
|
||||||
|
|
||||||
|
jfloat* gainsPtr = env->GetFloatArrayElements(gains, nullptr);
|
||||||
|
if (!gainsPtr) return;
|
||||||
|
|
||||||
|
for (int b = 0; b < bandsToUpdate; b++) {
|
||||||
|
gEqL[b].setTargetGain(gainsPtr[b]);
|
||||||
|
gEqR[b].setTargetGain(gainsPtr[b]);
|
||||||
}
|
}
|
||||||
gEqEnabled = gEqL.hasActiveBands();
|
|
||||||
|
gEqUpdateCounter = 1;
|
||||||
|
|
||||||
|
env->ReleaseFloatArrayElements(gains, gainsPtr, JNI_ABORT);
|
||||||
}
|
}
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setBassBoost(JNIEnv*, jobject, jfloat g) {
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setBassBoost(JNIEnv*, jobject, jfloat g) {
|
||||||
if (g > 0.01f) {
|
float scaledGain = g * 4.0f;
|
||||||
gBassL.setLowShelf(44100.0f, 150.0f, g, SQRT_2_INV);
|
gBassL.targetGain.store(scaledGain, std::memory_order_release);
|
||||||
gBassR.setLowShelf(44100.0f, 150.0f, g, SQRT_2_INV);
|
gBassR.targetGain.store(scaledGain, std::memory_order_release);
|
||||||
gBassBoostEnabled = true;
|
float sr = gSampleRate.load(std::memory_order_acquire);
|
||||||
} else { gBassBoostEnabled = false; }
|
gBassL.applyGain(sr);
|
||||||
|
gBassR.applyGain(sr);
|
||||||
|
if (std::abs(g) > 0.01f) {
|
||||||
|
gBassL.active.store(true, std::memory_order_release);
|
||||||
|
gBassR.active.store(true, std::memory_order_release);
|
||||||
|
} else {
|
||||||
|
gBassL.active.store(false, std::memory_order_release);
|
||||||
|
gBassR.active.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setStereoWidth(JNIEnv*, jobject, jfloat w) {
|
||||||
|
gStereoWidth.store(fmaxf(0.0f, fminf(w, 2.0f)), std::memory_order_release);
|
||||||
}
|
}
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setStereoWidth(JNIEnv*, jobject, jfloat w) { gStereoWidth = fmaxf(0.0f, fminf(w, 2.0f)); }
|
|
||||||
|
|
||||||
JNIEXPORT jfloatArray JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_getFftData(JNIEnv* env, jobject) {
|
JNIEXPORT jfloatArray JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_getFftData(JNIEnv* env, jobject) {
|
||||||
jfloatArray arr = env->NewFloatArray(256);
|
jfloatArray arr = env->NewFloatArray(256);
|
||||||
@@ -242,46 +356,103 @@ JNIEXPORT jfloatArray JNICALL Java_com_michatec_radio_helpers_NativeAudioProcess
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void computeLogarithmicFFT(float* output, const std::complex<float>* input, int inputSize) {
|
||||||
|
float sr = gSampleRate.load(std::memory_order_acquire);
|
||||||
|
float binWidth = sr / (2.0f * static_cast<float>(inputSize));
|
||||||
|
constexpr int NUM_BANDS = 256;
|
||||||
|
constexpr float MIN_FREQ = 20.0f;
|
||||||
|
constexpr float MAX_FREQ = 20000.0f;
|
||||||
|
float logMin = logf(MIN_FREQ);
|
||||||
|
float logMax = logf(MAX_FREQ);
|
||||||
|
float logRange = logMax - logMin;
|
||||||
|
|
||||||
|
for (int b = 0; b < NUM_BANDS; b++) {
|
||||||
|
float f1 = expf(logMin + (logRange * static_cast<float>(b) / static_cast<float>(NUM_BANDS)));
|
||||||
|
float f2 = expf(logMin + (logRange * static_cast<float>(b + 1) / static_cast<float>(NUM_BANDS)));
|
||||||
|
int idx1 = static_cast<int>(f1 / binWidth);
|
||||||
|
int idx2 = static_cast<int>(f2 / binWidth);
|
||||||
|
idx1 = std::max(0, std::min(idx1, inputSize - 1));
|
||||||
|
idx2 = std::max(0, std::min(idx2, inputSize - 1));
|
||||||
|
|
||||||
|
float sum = 0.0f;
|
||||||
|
int count = idx2 - idx1 + 1;
|
||||||
|
for (int i = idx1; i <= idx2 && i < inputSize; i++) {
|
||||||
|
sum += std::abs(input[i]);
|
||||||
|
}
|
||||||
|
float avg = (count > 0) ? sum / static_cast<float>(count) : 0.0f;
|
||||||
|
output[b] = avg * 0.5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_processAudioDirect(JNIEnv* env, jobject, jobject byteBuffer, jint size) {
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_processAudioDirect(JNIEnv* env, jobject, jobject byteBuffer, jint size) {
|
||||||
auto* buffer = static_cast<jshort*>(env->GetDirectBufferAddress(byteBuffer));
|
auto* buffer = static_cast<jshort*>(env->GetDirectBufferAddress(byteBuffer));
|
||||||
if (!buffer) return;
|
if (!buffer) return;
|
||||||
int numFrames = (size / 2) / 2;
|
int numFrames = (size / 2) / 2;
|
||||||
if (numFrames > 4096) numFrames = 4096;
|
if (numFrames > 4096) numFrames = 4096;
|
||||||
|
|
||||||
for (int i = 0; i < numFrames; i++) {
|
if (gEqUpdateCounter > 0) {
|
||||||
gLeftBuf[i] = static_cast<float>(buffer[i * 2]) * INV_32768;
|
updateAllEqBands();
|
||||||
gRightBuf[i] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768;
|
gEqUpdateCounter--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gEqEnabled) { gEqL.processBlock(gLeftBuf.data(), numFrames); gEqR.processBlock(gRightBuf.data(), numFrames); }
|
for (int i = 0; i < numFrames; i++) {
|
||||||
if (gBassBoostEnabled) {
|
gLeftBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2]) * INV_32768;
|
||||||
for(int i=0; i<numFrames; i++) { gLeftBuf[i] = gBassL.process(gLeftBuf[i]); gRightBuf[i] = gBassR.process(gRightBuf[i]); }
|
gRightBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < numFrames; i++) {
|
||||||
|
float xL = gLeftBuf[static_cast<size_t>(i)];
|
||||||
|
float xR = gRightBuf[static_cast<size_t>(i)];
|
||||||
|
|
||||||
|
for (int b = 0; b < NUM_EQ_BANDS; b++) {
|
||||||
|
xL = gEqL[b].process(xL);
|
||||||
|
xR = gEqR[b].process(xR);
|
||||||
|
}
|
||||||
|
|
||||||
|
gLeftBuf[static_cast<size_t>(i)] = xL;
|
||||||
|
gRightBuf[static_cast<size_t>(i)] = xR;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < numFrames; i++) {
|
||||||
|
gLeftBuf[static_cast<size_t>(i)] = gBassL.process(gLeftBuf[static_cast<size_t>(i)]);
|
||||||
|
gRightBuf[static_cast<size_t>(i)] = gBassR.process(gRightBuf[static_cast<size_t>(i)]);
|
||||||
|
}
|
||||||
|
|
||||||
gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
||||||
|
|
||||||
if (gStereoWidth != 1.0f) {
|
float stereoWidth = gStereoWidth.load(std::memory_order_relaxed);
|
||||||
float halfWidth = gStereoWidth * 0.5f;
|
if (stereoWidth != 1.0f) {
|
||||||
|
float halfWidth = stereoWidth * 0.5f;
|
||||||
for (int j = 0; j < numFrames; j++) {
|
for (int j = 0; j < numFrames; j++) {
|
||||||
float mid = (gLeftBuf[j] + gRightBuf[j]) * 0.5f;
|
float mid = (gLeftBuf[static_cast<size_t>(j)] + gRightBuf[static_cast<size_t>(j)]) * 0.5f;
|
||||||
float side = (gLeftBuf[j] - gRightBuf[j]) * halfWidth;
|
float side = (gLeftBuf[static_cast<size_t>(j)] - gRightBuf[static_cast<size_t>(j)]) * halfWidth;
|
||||||
gLeftBuf[j] = mid + side; gRightBuf[j] = mid - side;
|
gLeftBuf[static_cast<size_t>(j)] = mid + side;
|
||||||
|
gRightBuf[static_cast<size_t>(j)] = mid - side;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gDrcEnabled) gCompressor.process(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
gCompressor.process(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
||||||
|
|
||||||
// FFT for visualization
|
if (numFrames >= FFT_SIZE) {
|
||||||
for (int k = 0; k < FFT_SIZE; k++) {
|
for (int k = 0; k < FFT_SIZE; k++) {
|
||||||
gFFTWork[k] = (k < 256 && k < numFrames) ? std::complex<float>(gLeftBuf[k], 0.0f) : std::complex<float>(0.0f, 0.0f);
|
gFFTWork[static_cast<size_t>(k)] = std::complex<float>(gLeftBuf[static_cast<size_t>(k)], 0.0f);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (int k = 0; k < numFrames; k++) {
|
||||||
|
gFFTWork[static_cast<size_t>(k)] = std::complex<float>(gLeftBuf[static_cast<size_t>(k)], 0.0f);
|
||||||
|
}
|
||||||
|
for (int k = numFrames; k < FFT_SIZE; k++) {
|
||||||
|
gFFTWork[static_cast<size_t>(k)] = std::complex<float>(0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyHannWindowToReal(gFFTWork.data(), FFT_SIZE);
|
||||||
fastFFT(gFFTWork.data(), FFT_SIZE);
|
fastFFT(gFFTWork.data(), FFT_SIZE);
|
||||||
for (int k = 0; k < 256; k++) {
|
computeLogarithmicFFT(gFFTData.data(), gFFTWork.data(), FFT_SIZE / 2);
|
||||||
gFFTData[k] = std::abs(gFFTWork[k]) * 0.5f; // Increased scale
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int k = 0; k < numFrames; k++) {
|
for (int k = 0; k < numFrames; k++) {
|
||||||
buffer[k * 2] = static_cast<jshort>(fastSoftClip(gLeftBuf[k]) * 32767.0f);
|
buffer[k * 2] = static_cast<jshort>(fastSoftClip(gLeftBuf[static_cast<size_t>(k)]) * 32767.0f);
|
||||||
buffer[k * 2 + 1] = static_cast<jshort>(fastSoftClip(gRightBuf[k]) * 32767.0f);
|
buffer[k * 2 + 1] = static_cast<jshort>(fastSoftClip(gRightBuf[static_cast<size_t>(k)]) * 32767.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-11
@@ -4,7 +4,7 @@
|
|||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
@@ -29,32 +29,48 @@ Java_com_michatec_radio_helpers_ExtrasHelper_visualize(JNIEnv *env, jclass clazz
|
|||||||
|
|
||||||
// Clear background (Dark Grey)
|
// Clear background (Dark Grey)
|
||||||
for (int y = 0; y < buffer.height; y++) {
|
for (int y = 0; y < buffer.height; y++) {
|
||||||
|
uint32_t* row = pixels + (y * buffer.stride);
|
||||||
for (int x = 0; x < buffer.width; x++) {
|
for (int x = 0; x < buffer.width; x++) {
|
||||||
pixels[y * buffer.stride + x] = 0xFF121212;
|
row[x] = 0xFF121212;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw bars
|
// Draw bars - fewer bins = thicker bars
|
||||||
int displayBins = std::min(static_cast<int>(len), 128);
|
int displayBins = 40;
|
||||||
float barWidth = static_cast<float>(buffer.width) / static_cast<float>(displayBins);
|
float barWidth = static_cast<float>(buffer.width) / static_cast<float>(displayBins);
|
||||||
|
int padding = static_cast<int>(barWidth * 0.2f);
|
||||||
|
if (padding < 1) padding = 1;
|
||||||
|
|
||||||
for (int i = 0; i < displayBins; i++) {
|
for (int i = 0; i < displayBins; i++) {
|
||||||
// Keep original order: bass (low freq) at left, treble (high freq) at right
|
// Map display bin to data index
|
||||||
float val = body[i];
|
int dataIdx = (i * len) / displayBins;
|
||||||
float scaledVal = val * 5.0f;
|
float val = body[dataIdx];
|
||||||
|
|
||||||
|
// Use square root to compress the range (so peaks don't hit the top too easily)
|
||||||
|
// and a lower multiplier (0.4f) to reduce overall height
|
||||||
|
float scaledVal = sqrtf(val) * 0.5f;
|
||||||
int barHeight = static_cast<int>(scaledVal * static_cast<float>(buffer.height));
|
int barHeight = static_cast<int>(scaledVal * static_cast<float>(buffer.height));
|
||||||
|
|
||||||
if (barHeight > buffer.height) barHeight = buffer.height;
|
// Cap height at 75% to leave some room at the top
|
||||||
if (barHeight < 12) barHeight = 12; // Min height
|
int maxH = static_cast<int>(static_cast<float>(buffer.height) * 0.75f);
|
||||||
|
if (barHeight > maxH) barHeight = maxH;
|
||||||
|
if (barHeight < 4) barHeight = 4; // Minimal visible line
|
||||||
|
|
||||||
int startX = static_cast<int>(static_cast<float>(i) * barWidth);
|
int startX = static_cast<int>(static_cast<float>(i) * barWidth);
|
||||||
int endX = static_cast<int>(static_cast<float>(i + 1) * barWidth);
|
int endX = static_cast<int>(static_cast<float>(i + 1) * barWidth);
|
||||||
int barBottom = buffer.height;
|
|
||||||
|
int drawStartX = startX + padding;
|
||||||
|
int drawEndX = endX - padding;
|
||||||
|
if (drawEndX <= drawStartX) drawEndX = drawStartX + 1;
|
||||||
|
|
||||||
|
int barBottom = buffer.height - 4; // Bottom margin
|
||||||
int barTop = barBottom - barHeight;
|
int barTop = barBottom - barHeight;
|
||||||
|
|
||||||
for (int x = startX; x <= endX; x++) {
|
for (int x = drawStartX; x < drawEndX; x++) {
|
||||||
if (x < 0 || x >= buffer.width) continue;
|
if (x < 0 || x >= buffer.width) continue;
|
||||||
for (int y = barTop; y < barBottom; y++) {
|
for (int y = barTop; y < barBottom; y++) {
|
||||||
|
if (y < 0 || y >= buffer.height) continue;
|
||||||
|
// Using the same color, but now height is controlled
|
||||||
pixels[y * buffer.stride + x] = 0xFFC5DA03;
|
pixels[y * buffer.stride + x] = 0xFFC5DA03;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.michatec.radio
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.textview.MaterialTextView
|
||||||
|
import com.michatec.radio.collection.CollectionViewModel
|
||||||
|
import com.michatec.radio.core.Station
|
||||||
|
import com.michatec.radio.helpers.CollectionHelper
|
||||||
|
import com.michatec.radio.helpers.NetworkHelper
|
||||||
|
import com.michatec.radio.search.DirectInputCheck
|
||||||
|
import com.michatec.radio.search.RadioBrowserResult
|
||||||
|
import com.michatec.radio.search.RadioBrowserSearch
|
||||||
|
import com.michatec.radio.search.SearchResultAdapter
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class AddStationFragment : Fragment(),
|
||||||
|
SearchResultAdapter.SearchResultAdapterListener,
|
||||||
|
RadioBrowserSearch.RadioBrowserSearchListener,
|
||||||
|
DirectInputCheck.DirectInputCheckListener {
|
||||||
|
|
||||||
|
private lateinit var collectionViewModel: CollectionViewModel
|
||||||
|
private lateinit var stationSearchBoxView: SearchView
|
||||||
|
private lateinit var searchRequestProgressIndicator: ProgressBar
|
||||||
|
private lateinit var noSearchResultsTextView: MaterialTextView
|
||||||
|
private lateinit var stationSearchResultList: RecyclerView
|
||||||
|
private lateinit var positiveButton: Button
|
||||||
|
private lateinit var negativeButton: Button
|
||||||
|
private lateinit var searchResultAdapter: SearchResultAdapter
|
||||||
|
private lateinit var radioBrowserSearch: RadioBrowserSearch
|
||||||
|
private lateinit var directInputCheck: DirectInputCheck
|
||||||
|
private var station: Station = Station()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
// We reuse the dialog layout as it's already optimized for TV in layout-television
|
||||||
|
val view = inflater.inflate(R.layout.dialog_find_station, container, false)
|
||||||
|
|
||||||
|
collectionViewModel = ViewModelProvider(requireActivity())[CollectionViewModel::class.java]
|
||||||
|
radioBrowserSearch = RadioBrowserSearch(this)
|
||||||
|
directInputCheck = DirectInputCheck(this)
|
||||||
|
|
||||||
|
stationSearchBoxView = view.findViewById(R.id.station_search_box_view)
|
||||||
|
searchRequestProgressIndicator = view.findViewById(R.id.search_request_progress_indicator)
|
||||||
|
stationSearchResultList = view.findViewById(R.id.station_search_result_list)
|
||||||
|
noSearchResultsTextView = view.findViewById(R.id.no_results_text_view)
|
||||||
|
positiveButton = view.findViewById(R.id.dialog_positive_button)
|
||||||
|
negativeButton = view.findViewById(R.id.dialog_negative_button)
|
||||||
|
|
||||||
|
setupRecyclerView()
|
||||||
|
setupSearchView()
|
||||||
|
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
addStationAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
negativeButton.setOnClickListener {
|
||||||
|
searchResultAdapter.stopPrePlayback()
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
stationSearchBoxView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextChange(query: String): Boolean {
|
||||||
|
handleSearch(query)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
|
handleSearch(query)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
// Stop playback when fragment is destroyed (e.g. via back button)
|
||||||
|
if (this::searchResultAdapter.isInitialized) {
|
||||||
|
searchResultAdapter.stopPrePlayback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
searchResultAdapter = SearchResultAdapter(this, listOf())
|
||||||
|
stationSearchResultList.adapter = searchResultAdapter
|
||||||
|
stationSearchResultList.layoutManager = LinearLayoutManager(context)
|
||||||
|
stationSearchResultList.itemAnimator = DefaultItemAnimator()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSearchView() {
|
||||||
|
// TV specific: ensure keyboard opens when search view gets focus
|
||||||
|
stationSearchBoxView.setOnQueryTextFocusChangeListener { v, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
// Find the internal EditText of the SearchView
|
||||||
|
val searchEditText = v.findViewById<EditText>(androidx.appcompat.R.id.search_src_text)
|
||||||
|
if (searchEditText != null) {
|
||||||
|
searchEditText.requestFocus()
|
||||||
|
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the SearchView always expanded and ready for input
|
||||||
|
stationSearchBoxView.isIconified = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSearch(query: String) {
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
resetLayout(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showProgressIndicator()
|
||||||
|
if (query.startsWith("http")) {
|
||||||
|
directInputCheck.checkStationAddress(requireContext(), query)
|
||||||
|
} else {
|
||||||
|
radioBrowserSearch.searchStation(requireContext(), query, Keys.SEARCH_TYPE_BY_KEYWORD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addStationAndExit() {
|
||||||
|
searchResultAdapter.stopPrePlayback()
|
||||||
|
val currentCollection = collectionViewModel.collectionLiveData.value ?: return
|
||||||
|
if (station.streamContent.isNotEmpty() && station.streamContent != Keys.MIME_TYPE_UNSUPPORTED) {
|
||||||
|
CollectionHelper.addStation(requireContext(), currentCollection, station)
|
||||||
|
findNavController().navigateUp()
|
||||||
|
} else {
|
||||||
|
CoroutineScope(IO).launch {
|
||||||
|
val contentType = NetworkHelper.detectContentType(station.getStreamUri())
|
||||||
|
station.streamContent = contentType.type
|
||||||
|
withContext(Main) {
|
||||||
|
CollectionHelper.addStation(requireContext(), currentCollection, station)
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearchResultTapped(result: Station) {
|
||||||
|
station = result
|
||||||
|
activateAddButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun activateAddButton() {
|
||||||
|
positiveButton.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deactivateAddButton() {
|
||||||
|
positiveButton.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onRadioBrowserSearchResults(results: Array<RadioBrowserResult>) {
|
||||||
|
if (results.isNotEmpty()) {
|
||||||
|
searchResultAdapter.searchResults = results.map { it.toStation() }
|
||||||
|
searchResultAdapter.notifyDataSetChanged()
|
||||||
|
resetLayout(false)
|
||||||
|
} else {
|
||||||
|
showNoResultsError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onDirectInputCheck(stationList: MutableList<Station>) {
|
||||||
|
if (stationList.isNotEmpty()) {
|
||||||
|
searchResultAdapter.searchResults = stationList
|
||||||
|
searchResultAdapter.notifyDataSetChanged()
|
||||||
|
resetLayout(false)
|
||||||
|
} else {
|
||||||
|
showNoResultsError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetLayout(clear: Boolean) {
|
||||||
|
positiveButton.isEnabled = false
|
||||||
|
searchRequestProgressIndicator.isGone = true
|
||||||
|
noSearchResultsTextView.isGone = true
|
||||||
|
if (clear) searchResultAdapter.resetSelection(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showProgressIndicator() {
|
||||||
|
searchRequestProgressIndicator.isVisible = true
|
||||||
|
noSearchResultsTextView.isGone = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNoResultsError() {
|
||||||
|
searchRequestProgressIndicator.isGone = true
|
||||||
|
noSearchResultsTextView.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,40 @@
|
|||||||
package com.michatec.radio
|
package com.michatec.radio
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
|
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
|
||||||
|
import com.michatec.radio.helpers.PreferencesHelper
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class ExpandedControllerActivity : ExpandedControllerActivity() {
|
class ExpandedControllerActivity : ExpandedControllerActivity() {
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
val languageCode = PreferencesHelper.loadSelectedLanguage()
|
||||||
|
val context = if (languageCode.isEmpty() || languageCode == "system") {
|
||||||
|
// Use system default locale
|
||||||
|
newBase
|
||||||
|
} else {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
val config = Configuration(newBase.resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
newBase.createConfigurationContext(config)
|
||||||
|
}
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
menuInflater.inflate(R.menu.expanded_controller, menu)
|
menuInflater.inflate(R.menu.expanded_controller, menu)
|
||||||
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
|
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
try {
|
||||||
|
super.onResume()
|
||||||
|
} catch (_: ClassCastException) {
|
||||||
|
// Fix for lifecycle exception on some devices (e.g. Samsung)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ object Keys {
|
|||||||
const val PREF_EQ_BAND_5: String = "EQ_BAND_5"
|
const val PREF_EQ_BAND_5: String = "EQ_BAND_5"
|
||||||
const val PREF_EQ_BAND_6: String = "EQ_BAND_6"
|
const val PREF_EQ_BAND_6: String = "EQ_BAND_6"
|
||||||
const val PREF_EQ_BAND_7: String = "EQ_BAND_7"
|
const val PREF_EQ_BAND_7: String = "EQ_BAND_7"
|
||||||
const val PREF_EQ_BAND_8: String = "EQ_BAND_8"
|
|
||||||
const val PREF_PRESET_SELECTED: String = "PRESET_SELECTED"
|
const val PREF_PRESET_SELECTED: String = "PRESET_SELECTED"
|
||||||
const val PREF_PRESET_EQ_BAND_0: String = "PRESET_EQ_BAND_0"
|
const val PREF_PRESET_EQ_BAND_0: String = "PRESET_EQ_BAND_0"
|
||||||
const val PREF_PRESET_EQ_BAND_1: String = "PRESET_EQ_BAND_1"
|
const val PREF_PRESET_EQ_BAND_1: String = "PRESET_EQ_BAND_1"
|
||||||
@@ -92,6 +91,7 @@ object Keys {
|
|||||||
const val PREF_PRESET_REVERB: String = "PRESET_REVERB"
|
const val PREF_PRESET_REVERB: String = "PRESET_REVERB"
|
||||||
const val PREF_PRESET_DRC: String = "PRESET_DRC"
|
const val PREF_PRESET_DRC: String = "PRESET_DRC"
|
||||||
const val PREF_PRESET_STEREO_WIDTH: String = "PRESET_STEREO_WIDTH"
|
const val PREF_PRESET_STEREO_WIDTH: String = "PRESET_STEREO_WIDTH"
|
||||||
|
const val PREF_LANGUAGE_SELECTED: String = "PRESET_LANGUAGE_SELECTED"
|
||||||
|
|
||||||
// default const values
|
// default const values
|
||||||
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
|
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.michatec.radio
|
package com.michatec.radio
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -16,8 +18,10 @@ import androidx.navigation.ui.NavigationUI
|
|||||||
import androidx.navigation.ui.navigateUp
|
import androidx.navigation.ui.navigateUp
|
||||||
import com.michatec.radio.helpers.AppThemeHelper
|
import com.michatec.radio.helpers.AppThemeHelper
|
||||||
import com.michatec.radio.helpers.FileHelper
|
import com.michatec.radio.helpers.FileHelper
|
||||||
|
import com.michatec.radio.helpers.LanguageHelper
|
||||||
import com.michatec.radio.helpers.PreferencesHelper
|
import com.michatec.radio.helpers.PreferencesHelper
|
||||||
import org.woheller69.freeDroidWarn.FreeDroidWarn
|
import org.woheller69.freeDroidWarn.FreeDroidWarn
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MainActivity class
|
* MainActivity class
|
||||||
@@ -27,6 +31,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
/* Main class variables */
|
/* Main class variables */
|
||||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||||
|
|
||||||
|
/* Overrides attachBaseContext from AppCompatActivity */
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
val languageCode = PreferencesHelper.loadSelectedLanguage()
|
||||||
|
val context = if (languageCode.isEmpty() || languageCode == "system") {
|
||||||
|
// Use system default locale
|
||||||
|
newBase
|
||||||
|
} else {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
val config = Configuration(newBase.resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
newBase.createConfigurationContext(config)
|
||||||
|
}
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
/* Overrides onCreate from AppCompatActivity */
|
/* Overrides onCreate from AppCompatActivity */
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
@@ -112,6 +131,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Keys.PREF_THEME_SELECTION -> {
|
Keys.PREF_THEME_SELECTION -> {
|
||||||
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
|
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
|
||||||
}
|
}
|
||||||
|
Keys.PREF_LANGUAGE_SELECTED -> {
|
||||||
|
LanguageHelper.setLanguage(this, PreferencesHelper.loadSelectedLanguage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionResult
|
import androidx.media3.session.SessionResult
|
||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.android.volley.Request
|
import com.android.volley.Request
|
||||||
@@ -342,8 +343,15 @@ class PlayerFragment : Fragment(),
|
|||||||
|
|
||||||
/* Overrides onAddNewButtonTapped from CollectionAdapterListener */
|
/* Overrides onAddNewButtonTapped from CollectionAdapterListener */
|
||||||
override fun onAddNewButtonTapped() {
|
override fun onAddNewButtonTapped() {
|
||||||
|
// stop playback when adding a new station
|
||||||
|
controller?.stop()
|
||||||
|
|
||||||
|
if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_LEANBACK) == true) {
|
||||||
|
findNavController().navigate(R.id.action_map_fragment_to_player_to_add_station)
|
||||||
|
} else {
|
||||||
FindStationDialog(activity as Activity, this as FindStationDialog.FindStationDialogListener).show()
|
FindStationDialog(activity as Activity, this as FindStationDialog.FindStationDialogListener).show()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides onChangeImageButtonTapped from CollectionAdapterListener */
|
/* Overrides onChangeImageButtonTapped from CollectionAdapterListener */
|
||||||
@@ -625,6 +633,8 @@ class PlayerFragment : Fragment(),
|
|||||||
}
|
}
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
if (stationList.isNotEmpty()) {
|
if (stationList.isNotEmpty()) {
|
||||||
|
// stop playback when adding a new station via intent
|
||||||
|
controller?.stop()
|
||||||
AddStationDialog(activity as Activity, stationList, this@PlayerFragment as AddStationDialog.AddStationDialogListener).show()
|
AddStationDialog(activity as Activity, stationList, this@PlayerFragment as AddStationDialog.AddStationDialogListener).show()
|
||||||
} else {
|
} else {
|
||||||
// invalid address
|
// invalid address
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
|||||||
Keys.PREF_EQ_LOW, Keys.PREF_EQ_MID, Keys.PREF_EQ_HIGH,
|
Keys.PREF_EQ_LOW, Keys.PREF_EQ_MID, Keys.PREF_EQ_HIGH,
|
||||||
Keys.PREF_EQ_BAND_1, Keys.PREF_EQ_BAND_2, Keys.PREF_EQ_BAND_3,
|
Keys.PREF_EQ_BAND_1, Keys.PREF_EQ_BAND_2, Keys.PREF_EQ_BAND_3,
|
||||||
Keys.PREF_EQ_BAND_4, Keys.PREF_EQ_BAND_5, Keys.PREF_EQ_BAND_6,
|
Keys.PREF_EQ_BAND_4, Keys.PREF_EQ_BAND_5, Keys.PREF_EQ_BAND_6,
|
||||||
Keys.PREF_EQ_BAND_7, Keys.PREF_EQ_BAND_8,
|
Keys.PREF_EQ_BAND_7,
|
||||||
Keys.PREF_PRESET_SELECTED,
|
Keys.PREF_PRESET_SELECTED,
|
||||||
Keys.PREF_PRESET_EQ_BAND_0, Keys.PREF_PRESET_EQ_BAND_1, Keys.PREF_PRESET_EQ_BAND_2,
|
Keys.PREF_PRESET_EQ_BAND_0, Keys.PREF_PRESET_EQ_BAND_1, Keys.PREF_PRESET_EQ_BAND_2,
|
||||||
Keys.PREF_PRESET_EQ_BAND_3, Keys.PREF_PRESET_EQ_BAND_4, Keys.PREF_PRESET_EQ_BAND_5,
|
Keys.PREF_PRESET_EQ_BAND_3, Keys.PREF_PRESET_EQ_BAND_4, Keys.PREF_PRESET_EQ_BAND_5,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.michatec.radio
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.michatec.radio.helpers.AppThemeHelper
|
import com.michatec.radio.helpers.AppThemeHelper
|
||||||
|
import com.michatec.radio.helpers.LanguageHelper
|
||||||
import com.michatec.radio.helpers.PreferencesHelper
|
import com.michatec.radio.helpers.PreferencesHelper
|
||||||
import com.michatec.radio.helpers.PreferencesHelper.initPreferences
|
import com.michatec.radio.helpers.PreferencesHelper.initPreferences
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ class Radio : Application() {
|
|||||||
initPreferences()
|
initPreferences()
|
||||||
// set Dark / Light theme state
|
// set Dark / Light theme state
|
||||||
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
|
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
|
||||||
|
LanguageHelper.setLanguage(this, PreferencesHelper.loadSelectedLanguage())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.navigation.fragment.findNavController
|
|||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.michatec.radio.dialogs.ErrorDialog
|
import com.michatec.radio.dialogs.ErrorDialog
|
||||||
|
import com.michatec.radio.dialogs.LanguageSelectionDialog
|
||||||
import com.michatec.radio.dialogs.PresetSelectionDialog
|
import com.michatec.radio.dialogs.PresetSelectionDialog
|
||||||
import com.michatec.radio.dialogs.ThemeSelectionDialog
|
import com.michatec.radio.dialogs.ThemeSelectionDialog
|
||||||
import com.michatec.radio.dialogs.YesNoDialog
|
import com.michatec.radio.dialogs.YesNoDialog
|
||||||
@@ -31,7 +32,7 @@ import java.util.*
|
|||||||
/*
|
/*
|
||||||
* SettingsFragment class
|
* SettingsFragment class
|
||||||
*/
|
*/
|
||||||
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener, PresetSelectionDialog.PresetSelectionDialogListener {
|
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener, PresetSelectionDialog.PresetSelectionDialogListener, LanguageSelectionDialog.LanguageSelectionDialogListener {
|
||||||
|
|
||||||
|
|
||||||
/* Define log tag */
|
/* Define log tag */
|
||||||
@@ -165,7 +166,11 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||||||
preferenceEnableEditingStreamUri.summaryOn = getString(R.string.pref_edit_station_stream_summary_enabled)
|
preferenceEnableEditingStreamUri.summaryOn = getString(R.string.pref_edit_station_stream_summary_enabled)
|
||||||
preferenceEnableEditingStreamUri.summaryOff = getString(R.string.pref_edit_station_stream_summary_disabled)
|
preferenceEnableEditingStreamUri.summaryOff = getString(R.string.pref_edit_station_stream_summary_disabled)
|
||||||
preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled(context))
|
preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled(context))
|
||||||
preferenceEnableEditingStreamUri.isEnabled = PreferencesHelper.loadEditStreamUrisEnabled(context)
|
preferenceEnableEditingStreamUri.isEnabled = if (PreferencesHelper.loadEditStationsEnabled(context)) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
PreferencesHelper.loadEditStreamUrisEnabled(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// set up "Edit Stations" preference
|
// set up "Edit Stations" preference
|
||||||
@@ -219,7 +224,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||||||
// set up "Preset Selection" preference
|
// set up "Preset Selection" preference
|
||||||
val preferencePresetSelection = Preference(context)
|
val preferencePresetSelection = Preference(context)
|
||||||
preferencePresetSelection.title = getString(R.string.pref_preset_selection_title)
|
preferencePresetSelection.title = getString(R.string.pref_preset_selection_title)
|
||||||
preferencePresetSelection.setIcon(R.drawable.ic_music_note_24dp)
|
preferencePresetSelection.setIcon(R.drawable.ic_presets_24dp)
|
||||||
preferencePresetSelection.key = Keys.PREF_PRESET_SELECTED
|
preferencePresetSelection.key = Keys.PREF_PRESET_SELECTED
|
||||||
val presetSummary = currentPreset.ifEmpty {
|
val presetSummary = currentPreset.ifEmpty {
|
||||||
getString(R.string.pref_preset_none)
|
getString(R.string.pref_preset_none)
|
||||||
@@ -236,7 +241,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||||||
// set up "Equalizer" preference entry
|
// set up "Equalizer" preference entry
|
||||||
val preferenceEqualizer = Preference(context)
|
val preferenceEqualizer = Preference(context)
|
||||||
preferenceEqualizer.title = getString(R.string.pref_equalizer_title)
|
preferenceEqualizer.title = getString(R.string.pref_equalizer_title)
|
||||||
preferenceEqualizer.setIcon(R.drawable.ic_music_note_24dp)
|
preferenceEqualizer.setIcon(R.drawable.ic_equalizer_24dp)
|
||||||
preferenceEqualizer.key = Keys.PREF_EQUALIZER
|
preferenceEqualizer.key = Keys.PREF_EQUALIZER
|
||||||
if (currentPreset.isEmpty()) {
|
if (currentPreset.isEmpty()) {
|
||||||
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary)
|
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary)
|
||||||
@@ -252,7 +257,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||||||
|
|
||||||
val preferenceVisualizer = Preference(context)
|
val preferenceVisualizer = Preference(context)
|
||||||
preferenceVisualizer.title = getString(R.string.pref_visualizer_title)
|
preferenceVisualizer.title = getString(R.string.pref_visualizer_title)
|
||||||
preferenceVisualizer.setIcon(R.drawable.ic_music_note_24dp)
|
preferenceVisualizer.setIcon(R.drawable.ic_visualizer_24dp)
|
||||||
preferenceVisualizer.summary = getString(R.string.pref_visualizer_summary)
|
preferenceVisualizer.summary = getString(R.string.pref_visualizer_summary)
|
||||||
preferenceVisualizer.setOnPreferenceClickListener {
|
preferenceVisualizer.setOnPreferenceClickListener {
|
||||||
findNavController().navigate(R.id.action_settings_to_visualizer)
|
findNavController().navigate(R.id.action_settings_to_visualizer)
|
||||||
@@ -304,6 +309,18 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||||||
return@setOnPreferenceClickListener true
|
return@setOnPreferenceClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val preferenceLanguageSelection = Preference(context)
|
||||||
|
preferenceLanguageSelection.title = getString(R.string.pref_language_selection_title)
|
||||||
|
preferenceLanguageSelection.setIcon(R.drawable.ic_language_24dp)
|
||||||
|
preferenceLanguageSelection.key = Keys.PREF_LANGUAGE_SELECTED
|
||||||
|
preferenceLanguageSelection.summary = "${getString(R.string.pref_language_selection_summary)}: ${
|
||||||
|
LanguageHelper.getCurrentLanguage(activity as Context)
|
||||||
|
}"
|
||||||
|
preferenceLanguageSelection.setOnPreferenceClickListener {
|
||||||
|
LanguageSelectionDialog(this).show(activity as Context)
|
||||||
|
return@setOnPreferenceClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// set preference categories
|
// set preference categories
|
||||||
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
|
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
|
||||||
@@ -330,6 +347,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||||||
|
|
||||||
screen.addPreference(preferenceCategoryGeneral)
|
screen.addPreference(preferenceCategoryGeneral)
|
||||||
preferenceCategoryGeneral.addPreference(preferenceThemeSelection)
|
preferenceCategoryGeneral.addPreference(preferenceThemeSelection)
|
||||||
|
preferenceCategoryGeneral.addPreference(preferenceLanguageSelection)
|
||||||
|
|
||||||
screen.addPreference(preferenceCategoryAudioEffects)
|
screen.addPreference(preferenceCategoryAudioEffects)
|
||||||
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
|
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
|
||||||
@@ -397,6 +415,20 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overrides onLanguageSelectionDialog from LanguageSelectionDialogListener */
|
||||||
|
override fun onLanguageSelectionDialog(dialogResult: Boolean, selectedLanguage: String) {
|
||||||
|
if (dialogResult) {
|
||||||
|
// update summary
|
||||||
|
val languagePreference = findPreference<Preference>(Keys.PREF_LANGUAGE_SELECTED)
|
||||||
|
val languageSummary = if (selectedLanguage.isEmpty()) {
|
||||||
|
getString(R.string.pref_language_system)
|
||||||
|
} else {
|
||||||
|
LanguageHelper.getCurrentLanguage(activity as Context)
|
||||||
|
}
|
||||||
|
languagePreference?.summary = "${getString(R.string.pref_language_selection_summary)}: $languageSummary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Updates the enabled/disabled state of EQ controls based on preset selection */
|
/* Updates the enabled/disabled state of EQ controls based on preset selection */
|
||||||
private fun updateEqControlStates() {
|
private fun updateEqControlStates() {
|
||||||
val currentPreset = PreferencesHelper.loadSelectedPreset()
|
val currentPreset = PreferencesHelper.loadSelectedPreset()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.michatec.radio.collection
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
@@ -11,12 +10,12 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.CheckBox
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.widget.Group
|
import androidx.constraintlayout.widget.Group
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -169,6 +168,9 @@ class CollectionAdapter(
|
|||||||
addNewViewHolder.settingsButtonView.setOnClickListener {
|
addNewViewHolder.settingsButtonView.setOnClickListener {
|
||||||
it.findNavController().navigate(R.id.settings_destination)
|
it.findNavController().navigate(R.id.settings_destination)
|
||||||
}
|
}
|
||||||
|
addNewViewHolder.visualizerButtonView.setOnClickListener {
|
||||||
|
it.findNavController().navigate(R.id.visualizer_destination)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// CASE STATION CARD
|
// CASE STATION CARD
|
||||||
is StationViewHolder -> {
|
is StationViewHolder -> {
|
||||||
@@ -177,7 +179,6 @@ class CollectionAdapter(
|
|||||||
|
|
||||||
// get reference to StationViewHolder
|
// get reference to StationViewHolder
|
||||||
val stationViewHolder: StationViewHolder = holder
|
val stationViewHolder: StationViewHolder = holder
|
||||||
|
|
||||||
// set up station views
|
// set up station views
|
||||||
setStarredIcon(stationViewHolder, station)
|
setStarredIcon(stationViewHolder, station)
|
||||||
setStationName(stationViewHolder, station)
|
setStationName(stationViewHolder, station)
|
||||||
@@ -187,13 +188,12 @@ class CollectionAdapter(
|
|||||||
setPlaybackProgress(stationViewHolder, station)
|
setPlaybackProgress(stationViewHolder, station)
|
||||||
setDownloadProgress(stationViewHolder, station)
|
setDownloadProgress(stationViewHolder, station)
|
||||||
|
|
||||||
// highlight if reordering
|
|
||||||
if (reorderStationUuid == station.uuid) {
|
if (reorderStationUuid == station.uuid) {
|
||||||
stationViewHolder.stationCardView.setStrokeColor(
|
stationViewHolder.reorderCheckbox.isVisible = true
|
||||||
ColorStateList.valueOf(
|
stationViewHolder.reorderCheckbox.isChecked = true
|
||||||
ContextCompat.getColor(context, R.color.cardview_reordering)
|
} else {
|
||||||
)
|
stationViewHolder.reorderCheckbox.isGone = true
|
||||||
)
|
stationViewHolder.reorderCheckbox.isChecked = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// show / hide edit views
|
// show / hide edit views
|
||||||
@@ -339,6 +339,7 @@ class CollectionAdapter(
|
|||||||
|
|
||||||
|
|
||||||
/* Shows / hides the edit view for a station */
|
/* Shows / hides the edit view for a station */
|
||||||
|
/* TODO: Remove @SuppressLint("NotifyDataSetChanged"), remove NotifyDataSetChanged */
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun toggleEditViews(position: Int, stationUuid: String) {
|
private fun toggleEditViews(position: Int, stationUuid: String) {
|
||||||
when (stationUuid) {
|
when (stationUuid) {
|
||||||
@@ -754,6 +755,8 @@ class CollectionAdapter(
|
|||||||
listItemAddNewLayout.findViewById(R.id.card_add_new_station)
|
listItemAddNewLayout.findViewById(R.id.card_add_new_station)
|
||||||
val settingsButtonView: ExtendedFloatingActionButton =
|
val settingsButtonView: ExtendedFloatingActionButton =
|
||||||
listItemAddNewLayout.findViewById(R.id.card_settings)
|
listItemAddNewLayout.findViewById(R.id.card_settings)
|
||||||
|
val visualizerButtonView: ExtendedFloatingActionButton =
|
||||||
|
listItemAddNewLayout.findViewById(R.id.card_visualizer)
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* End of inner class
|
* End of inner class
|
||||||
@@ -769,10 +772,10 @@ class CollectionAdapter(
|
|||||||
val stationImageView: ImageView = stationCardLayout.findViewById(R.id.station_icon)
|
val stationImageView: ImageView = stationCardLayout.findViewById(R.id.station_icon)
|
||||||
val stationNameView: TextView = stationCardLayout.findViewById(R.id.station_name)
|
val stationNameView: TextView = stationCardLayout.findViewById(R.id.station_name)
|
||||||
val stationStarredView: ImageView = stationCardLayout.findViewById(R.id.starred_icon)
|
val stationStarredView: ImageView = stationCardLayout.findViewById(R.id.starred_icon)
|
||||||
|
val reorderCheckbox: CheckBox = stationCardLayout.findViewById(R.id.reorder_checkbox)
|
||||||
val bufferingProgress: ProgressBar = stationCardLayout.findViewById(R.id.buffering_progress)
|
val bufferingProgress: ProgressBar = stationCardLayout.findViewById(R.id.buffering_progress)
|
||||||
val downloadProgress: ProgressBar = stationCardLayout.findViewById(R.id.download_progress)
|
val downloadProgress: ProgressBar = stationCardLayout.findViewById(R.id.download_progress)
|
||||||
|
|
||||||
// val menuButtonView: ImageView = stationCardLayout.findViewById(R.id.menu_button)
|
|
||||||
val playButtonView: ImageView = stationCardLayout.findViewById(R.id.playback_button)
|
val playButtonView: ImageView = stationCardLayout.findViewById(R.id.playback_button)
|
||||||
val editViews: Group = stationCardLayout.findViewById(R.id.default_edit_views)
|
val editViews: Group = stationCardLayout.findViewById(R.id.default_edit_views)
|
||||||
val stationImageChangeView: ImageView =
|
val stationImageChangeView: ImageView =
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class FindStationDialog (
|
|||||||
|
|
||||||
|
|
||||||
/* Overrides onRadioBrowserSearchResults from RadioBrowserSearchListener */
|
/* Overrides onRadioBrowserSearchResults from RadioBrowserSearchListener */
|
||||||
|
/* TODO: Remove @SuppressLint("NotifyDataSetChanged"), remove NotifyDataSetChanged */
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onRadioBrowserSearchResults(results: Array<RadioBrowserResult>) {
|
override fun onRadioBrowserSearchResults(results: Array<RadioBrowserResult>) {
|
||||||
if (results.isNotEmpty()) {
|
if (results.isNotEmpty()) {
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package com.michatec.radio.dialogs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.RadioButton
|
||||||
|
import android.widget.RadioGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.michatec.radio.R
|
||||||
|
import com.michatec.radio.helpers.PreferencesHelper
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* LanguageSelectionDialog class
|
||||||
|
*/
|
||||||
|
class LanguageSelectionDialog(private var languageSelectionDialogListener: LanguageSelectionDialogListener) {
|
||||||
|
|
||||||
|
/* Interface used to communicate back to activity */
|
||||||
|
interface LanguageSelectionDialogListener {
|
||||||
|
fun onLanguageSelectionDialog(dialogResult: Boolean, selectedLanguage: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Main class variables */
|
||||||
|
private lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
|
||||||
|
/* Data class representing a supported language */
|
||||||
|
data class Language(
|
||||||
|
val code: String,
|
||||||
|
val nameResId: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/* List of supported languages - displayed in their own language */
|
||||||
|
private val supportedLanguages = listOf(
|
||||||
|
Language("system", R.string.pref_language_system),
|
||||||
|
Language("en", R.string.pref_language_en),
|
||||||
|
Language("de", R.string.pref_language_de),
|
||||||
|
Language("fr", R.string.pref_language_fr),
|
||||||
|
Language("ru", R.string.pref_language_ru),
|
||||||
|
Language("ja", R.string.pref_language_ja),
|
||||||
|
Language("nl", R.string.pref_language_nl),
|
||||||
|
Language("pl", R.string.pref_language_pl),
|
||||||
|
Language("el", R.string.pref_language_el),
|
||||||
|
Language("da", R.string.pref_language_da)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/* Counter for generating unique view IDs */
|
||||||
|
private var viewIdCounter = 0x7F010001 // Starting after android.R.id.home
|
||||||
|
|
||||||
|
|
||||||
|
/* Construct and show dialog */
|
||||||
|
fun show(context: Context) {
|
||||||
|
// prepare dialog builder
|
||||||
|
val builder = MaterialAlertDialogBuilder(context)
|
||||||
|
|
||||||
|
// inflate custom layout
|
||||||
|
val inflater = LayoutInflater.from(context)
|
||||||
|
val view = inflater.inflate(R.layout.dialog_language_selection, null)
|
||||||
|
|
||||||
|
// find radio group
|
||||||
|
val radioGroup = view.findViewById<RadioGroup>(R.id.language_radio_group)
|
||||||
|
val currentLanguage = PreferencesHelper.loadSelectedLanguage()
|
||||||
|
|
||||||
|
// add radio buttons for each supported language
|
||||||
|
for (language in supportedLanguages) {
|
||||||
|
val radioButton = RadioButton(context).apply {
|
||||||
|
id = generateViewId()
|
||||||
|
tag = language.code
|
||||||
|
text = context.getString(language.nameResId)
|
||||||
|
textSize = if (isTelevision(context)) 20f else 16f
|
||||||
|
setPadding(dpToPx(context, 8), dpToPx(context, 16), dpToPx(context, 16), dpToPx(context, 16))
|
||||||
|
}
|
||||||
|
radioGroup.addView(radioButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set current selection
|
||||||
|
for (i in 0 until radioGroup.childCount) {
|
||||||
|
val radioButton = radioGroup.getChildAt(i) as RadioButton
|
||||||
|
if (radioButton.tag == currentLanguage) {
|
||||||
|
radioButton.isChecked = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no language is selected, check the first one (system)
|
||||||
|
if (radioGroup.checkedRadioButtonId == -1) {
|
||||||
|
val firstButton = radioGroup.getChildAt(0) as RadioButton
|
||||||
|
firstButton.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up radio group listener
|
||||||
|
radioGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
val selectedButton = radioGroup.findViewById<RadioButton>(checkedId)
|
||||||
|
val selectedLanguageCode = selectedButton?.tag as? String ?: "system"
|
||||||
|
|
||||||
|
// save language selection to preferences
|
||||||
|
PreferencesHelper.saveSelectedLanguage(selectedLanguageCode)
|
||||||
|
|
||||||
|
// notify listener
|
||||||
|
languageSelectionDialogListener.onLanguageSelectionDialog(true, selectedLanguageCode)
|
||||||
|
|
||||||
|
// dismiss dialog
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// set custom view
|
||||||
|
builder.setView(view)
|
||||||
|
|
||||||
|
// handle outside-click as cancel
|
||||||
|
builder.setOnCancelListener {
|
||||||
|
languageSelectionDialogListener.onLanguageSelectionDialog(false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// display dialog
|
||||||
|
dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Generate a unique view ID */
|
||||||
|
private fun generateViewId(): Int {
|
||||||
|
return viewIdCounter++
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Helper function to check if device is a TV */
|
||||||
|
private fun isTelevision(context: Context): Boolean {
|
||||||
|
val uiMode = context.resources.configuration.uiMode
|
||||||
|
return (uiMode and android.content.res.Configuration.UI_MODE_TYPE_MASK) == android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Helper function to convert dp to pixels */
|
||||||
|
private fun dpToPx(context: Context, dp: Int): Int {
|
||||||
|
return (dp * context.resources.displayMetrics.density).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.michatec.radio.helpers
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.util.Log
|
||||||
|
import com.michatec.radio.R
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* LanguageHelper object
|
||||||
|
*/
|
||||||
|
object LanguageHelper {
|
||||||
|
|
||||||
|
/* Define log tag */
|
||||||
|
private val TAG: String = LanguageHelper::class.java.simpleName
|
||||||
|
|
||||||
|
|
||||||
|
/* Sets the app language on the activity */
|
||||||
|
fun setLanguage(context: Context, languageCode: String): Boolean {
|
||||||
|
if (languageCode.isEmpty()) {
|
||||||
|
Log.i(TAG, "No language code provided, using system default")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageCode == "system") {
|
||||||
|
Log.i(TAG, "Reverting to system default locale")
|
||||||
|
if (context is Activity) {
|
||||||
|
context.recreate()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
|
||||||
|
if (context is Activity) {
|
||||||
|
context.recreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Locale changed to: $languageCode")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Returns a localized resources object */
|
||||||
|
fun getCurrentLanguage(context: Context): String {
|
||||||
|
return when (val languageCode = PreferencesHelper.loadSelectedLanguage()) {
|
||||||
|
"system" -> context.getString(R.string.pref_language_system)
|
||||||
|
"en" -> context.getString(R.string.pref_language_en)
|
||||||
|
"de" -> context.getString(R.string.pref_language_de)
|
||||||
|
"fr" -> context.getString(R.string.pref_language_fr)
|
||||||
|
"ru" -> context.getString(R.string.pref_language_ru)
|
||||||
|
"ja" -> context.getString(R.string.pref_language_ja)
|
||||||
|
"nl" -> context.getString(R.string.pref_language_nl)
|
||||||
|
"pl" -> context.getString(R.string.pref_language_pl)
|
||||||
|
"el" -> context.getString(R.string.pref_language_el)
|
||||||
|
"da" -> context.getString(R.string.pref_language_da)
|
||||||
|
else -> languageCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,9 +27,10 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
private var directBuffer: ByteBuffer? = null
|
private var directBuffer: ByteBuffer? = null
|
||||||
|
|
||||||
// ===== JNI =====
|
// ===== JNI =====
|
||||||
|
private external fun setSampleRate(sampleRate: Float)
|
||||||
private external fun setDrcEnabled(enabled: Boolean)
|
private external fun setDrcEnabled(enabled: Boolean)
|
||||||
private external fun setReverbMix(mix: Float)
|
private external fun setReverbMix(mix: Float)
|
||||||
private external fun setEqBand(band: Int, gainDb: Float)
|
private external fun setEqFull(gains: FloatArray)
|
||||||
private external fun setBassBoost(gainDb: Float)
|
private external fun setBassBoost(gainDb: Float)
|
||||||
private external fun setStereoWidth(width: Float)
|
private external fun setStereoWidth(width: Float)
|
||||||
private external fun processAudioDirect(buf: ByteBuffer, size: Int)
|
private external fun processAudioDirect(buf: ByteBuffer, size: Int)
|
||||||
@@ -38,10 +39,7 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
// ===== API =====
|
// ===== API =====
|
||||||
fun enableDrc(enabled: Boolean) = setDrcEnabled(enabled)
|
fun enableDrc(enabled: Boolean) = setDrcEnabled(enabled)
|
||||||
fun setReverb(mix: Float) = setReverbMix(mix)
|
fun setReverb(mix: Float) = setReverbMix(mix)
|
||||||
fun setEq(band: Int, gainDb: Float) = setEqBand(band, gainDb)
|
fun setEqAll(gains: FloatArray) = setEqFull(gains)
|
||||||
fun setEqAll(gains: FloatArray) {
|
|
||||||
gains.forEachIndexed { i, g -> setEq(i, g) }
|
|
||||||
}
|
|
||||||
fun enableBassBoost(gainDb: Float) = setBassBoost(gainDb)
|
fun enableBassBoost(gainDb: Float) = setBassBoost(gainDb)
|
||||||
fun setWidth(width: Float) = setStereoWidth(width)
|
fun setWidth(width: Float) = setStereoWidth(width)
|
||||||
|
|
||||||
@@ -59,6 +57,8 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
Log.e(TAG, "Unsupported encoding: ${inputAudioFormat.encoding}")
|
Log.e(TAG, "Unsupported encoding: ${inputAudioFormat.encoding}")
|
||||||
throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
|
throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
|
||||||
}
|
}
|
||||||
|
// Pass the actual sample rate to native
|
||||||
|
setSampleRate(inputAudioFormat.sampleRate.toFloat())
|
||||||
return inputAudioFormat
|
return inputAudioFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,28 +66,27 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
val size = inputBuffer.remaining()
|
val size = inputBuffer.remaining()
|
||||||
if (size == 0) return
|
if (size == 0) return
|
||||||
|
|
||||||
// Always ensure we have a direct buffer for JNI
|
val bufferToProcess: ByteBuffer
|
||||||
|
if (inputBuffer.isDirect) {
|
||||||
|
bufferToProcess = inputBuffer
|
||||||
|
} else {
|
||||||
if (directBuffer == null || directBuffer!!.capacity() < size) {
|
if (directBuffer == null || directBuffer!!.capacity() < size) {
|
||||||
directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
|
directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
|
||||||
}
|
}
|
||||||
|
|
||||||
directBuffer!!.clear()
|
directBuffer!!.clear()
|
||||||
inputBuffer.position()
|
inputBuffer.position()
|
||||||
directBuffer!!.put(inputBuffer)
|
directBuffer!!.put(inputBuffer)
|
||||||
|
|
||||||
directBuffer!!.flip()
|
directBuffer!!.flip()
|
||||||
|
bufferToProcess = directBuffer!!
|
||||||
|
}
|
||||||
|
|
||||||
// Process audio in JNI
|
processAudioDirect(bufferToProcess, size)
|
||||||
processAudioDirect(directBuffer!!, size)
|
|
||||||
|
|
||||||
// Copy processed data back to output
|
|
||||||
val out = replaceOutputBuffer(size)
|
val out = replaceOutputBuffer(size)
|
||||||
out.order(ByteOrder.nativeOrder())
|
out.order(ByteOrder.nativeOrder())
|
||||||
|
bufferToProcess.position(0)
|
||||||
directBuffer!!.position(0)
|
out.put(bufferToProcess)
|
||||||
out.put(directBuffer!!)
|
|
||||||
out.flip()
|
out.flip()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReset() {
|
override fun onReset() {
|
||||||
@@ -98,24 +97,24 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
// ===== Presets =====
|
// ===== Presets =====
|
||||||
fun setPresetRock() {
|
fun setPresetRock() {
|
||||||
enableDrc(true)
|
enableDrc(true)
|
||||||
setReverb(0.10f)
|
setReverb(0.26f)
|
||||||
setWidth(1.1f)
|
setWidth(1.1f)
|
||||||
setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f))
|
setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f))
|
||||||
enableBassBoost(0.6f)
|
enableBassBoost(0.9f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPresetPop() {
|
fun setPresetPop() {
|
||||||
enableDrc(true)
|
enableDrc(true)
|
||||||
setReverb(0.15f)
|
setReverb(0.18f)
|
||||||
setWidth(1.05f)
|
setWidth(1.05f)
|
||||||
setEqAll(floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 1f))
|
setEqAll(floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 1f))
|
||||||
enableBassBoost(0.5f)
|
enableBassBoost(0.6f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPresetJazz() {
|
fun setPresetJazz() {
|
||||||
enableDrc(false)
|
enableDrc(false)
|
||||||
setReverb(0.15f)
|
setReverb(0.15f)
|
||||||
setWidth(1.0f)
|
setWidth(0.8f)
|
||||||
setEqAll(floatArrayOf(0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f, 0f, 0f))
|
setEqAll(floatArrayOf(0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f, 0f, 0f))
|
||||||
enableBassBoost(0.2f)
|
enableBassBoost(0.2f)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,13 +257,13 @@ object PreferencesHelper {
|
|||||||
|
|
||||||
/* Loads Bass Boost gain */
|
/* Loads Bass Boost gain */
|
||||||
fun loadBassBoost(): Float {
|
fun loadBassBoost(): Float {
|
||||||
return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 0.4f else 0.0f
|
return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 1f else 0.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Loads Reverb mix */
|
/* Loads Reverb mix */
|
||||||
fun loadReverb(): Float {
|
fun loadReverb(): Float {
|
||||||
return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.2f else 0.0f
|
return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.3f else 0.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -277,13 +277,13 @@ object PreferencesHelper {
|
|||||||
return when (band) {
|
return when (band) {
|
||||||
0 -> sharedPreferences.getInt(Keys.PREF_EQ_LOW, 0)
|
0 -> sharedPreferences.getInt(Keys.PREF_EQ_LOW, 0)
|
||||||
1 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_1, 0)
|
1 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_1, 0)
|
||||||
2 -> sharedPreferences.getInt(Keys.PREF_EQ_MID, 0)
|
2 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_2, 0)
|
||||||
3 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_3, 0)
|
3 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_3, 0)
|
||||||
4 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_4, 0)
|
4 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_4, 0)
|
||||||
5 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_5, 0)
|
5 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_5, 0)
|
||||||
6 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_6, 0)
|
6 -> sharedPreferences.getInt(Keys.PREF_EQ_MID, 0)
|
||||||
7 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_7, 0)
|
7 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_6, 0)
|
||||||
8 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_8, 0)
|
8 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_7, 0)
|
||||||
9 -> sharedPreferences.getInt(Keys.PREF_EQ_HIGH, 0)
|
9 -> sharedPreferences.getInt(Keys.PREF_EQ_HIGH, 0)
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
@@ -316,6 +316,16 @@ object PreferencesHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loads selected language */
|
||||||
|
fun loadSelectedLanguage(): String {
|
||||||
|
return sharedPreferences.getString(Keys.PREF_LANGUAGE_SELECTED, "system") ?: "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Saves selected language */
|
||||||
|
fun saveSelectedLanguage(language: String) {
|
||||||
|
sharedPreferences.edit { putString(Keys.PREF_LANGUAGE_SELECTED, language) }
|
||||||
|
}
|
||||||
|
|
||||||
/* Loads preset Bass Boost */
|
/* Loads preset Bass Boost */
|
||||||
fun loadPresetBassBoost(): Float {
|
fun loadPresetBassBoost(): Float {
|
||||||
return sharedPreferences.getFloat(Keys.PREF_PRESET_BASS_BOOST, 0f)
|
return sharedPreferences.getFloat(Keys.PREF_PRESET_BASS_BOOST, 0f)
|
||||||
@@ -340,7 +350,14 @@ object PreferencesHelper {
|
|||||||
fun resetEqualizer() {
|
fun resetEqualizer() {
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putInt(Keys.PREF_EQ_LOW, 0)
|
putInt(Keys.PREF_EQ_LOW, 0)
|
||||||
|
putInt(Keys.PREF_EQ_BAND_1, 0)
|
||||||
|
putInt(Keys.PREF_EQ_BAND_2, 0)
|
||||||
|
putInt(Keys.PREF_EQ_BAND_3, 0)
|
||||||
|
putInt(Keys.PREF_EQ_BAND_4, 0)
|
||||||
|
putInt(Keys.PREF_EQ_BAND_5, 0)
|
||||||
putInt(Keys.PREF_EQ_MID, 0)
|
putInt(Keys.PREF_EQ_MID, 0)
|
||||||
|
putInt(Keys.PREF_EQ_BAND_6, 0)
|
||||||
|
putInt(Keys.PREF_EQ_BAND_7, 0)
|
||||||
putInt(Keys.PREF_EQ_HIGH, 0)
|
putInt(Keys.PREF_EQ_HIGH, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.*
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.audio.AudioSink
|
||||||
|
import androidx.media3.exoplayer.audio.DefaultAudioSink
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.textview.MaterialTextView
|
import com.google.android.material.textview.MaterialTextView
|
||||||
import com.michatec.radio.R
|
import com.michatec.radio.R
|
||||||
import com.michatec.radio.core.Station
|
import com.michatec.radio.core.Station
|
||||||
|
import com.michatec.radio.helpers.NativeAudioProcessor
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -31,6 +37,7 @@ class SearchResultAdapter(
|
|||||||
private var exoPlayer: ExoPlayer? = null
|
private var exoPlayer: ExoPlayer? = null
|
||||||
private var paused: Boolean = false
|
private var paused: Boolean = false
|
||||||
private var isItemSelected: Boolean = false
|
private var isItemSelected: Boolean = false
|
||||||
|
private var nativeAudioProcessor = NativeAudioProcessor()
|
||||||
|
|
||||||
/* Listener Interface */
|
/* Listener Interface */
|
||||||
interface SearchResultAdapterListener {
|
interface SearchResultAdapterListener {
|
||||||
@@ -138,6 +145,7 @@ class SearchResultAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private fun performPrePlayback(context: Context, streamUri: String) {
|
private fun performPrePlayback(context: Context, streamUri: String) {
|
||||||
if (streamUri.contains(".m3u8")) {
|
if (streamUri.contains(".m3u8")) {
|
||||||
// release previous player if it exists
|
// release previous player if it exists
|
||||||
@@ -151,8 +159,30 @@ class SearchResultAdapter(
|
|||||||
// release previous player if it exists
|
// release previous player if it exists
|
||||||
stopPrePlayback()
|
stopPrePlayback()
|
||||||
|
|
||||||
// create a new instance of ExoPlayer
|
// set up audio attributes for the preview player
|
||||||
exoPlayer = ExoPlayer.Builder(context).build()
|
val audioAttributes = androidx.media3.common.AudioAttributes.Builder()
|
||||||
|
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||||
|
.setUsage(C.USAGE_MEDIA)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Create a RenderersFactory that injects the NativeAudioProcessor
|
||||||
|
val renderersFactory = object : DefaultRenderersFactory(context) {
|
||||||
|
override fun buildAudioSink(
|
||||||
|
context: Context,
|
||||||
|
enableFloatOutput: Boolean,
|
||||||
|
enableAudioTrackPlaybackParams: Boolean
|
||||||
|
): AudioSink? {
|
||||||
|
return DefaultAudioSink.Builder(context)
|
||||||
|
.setAudioProcessors(arrayOf(nativeAudioProcessor))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new instance of ExoPlayer with focus handling
|
||||||
|
exoPlayer = ExoPlayer.Builder(context, renderersFactory)
|
||||||
|
.setAudioAttributes(audioAttributes, true)
|
||||||
|
.setHandleAudioBecomingNoisy(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
// create a MediaItem with the streamUri
|
// create a MediaItem with the streamUri
|
||||||
val mediaItem = MediaItem.fromUri(streamUri)
|
val mediaItem = MediaItem.fromUri(streamUri)
|
||||||
@@ -199,7 +229,7 @@ class SearchResultAdapter(
|
|||||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||||
.setAudioAttributes(audioAttributes)
|
.setAudioAttributes(audioAttributes)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.michatec.radio.ui
|
package com.michatec.radio.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable
|
import android.graphics.drawable.AnimatedVectorDrawable
|
||||||
import android.os.Build
|
import java.util.Locale
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
@@ -129,7 +128,6 @@ data class LayoutHolder(var rootView: View) {
|
|||||||
|
|
||||||
|
|
||||||
/* Updates the player views */
|
/* Updates the player views */
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
fun updatePlayerViews(context: Context, station: Station, isPlaying: Boolean) {
|
fun updatePlayerViews(context: Context, station: Station, isPlaying: Boolean) {
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
metadataView?.text = station.name
|
metadataView?.text = station.name
|
||||||
@@ -162,9 +160,9 @@ data class LayoutHolder(var rootView: View) {
|
|||||||
} else {
|
} else {
|
||||||
val kiloBytesPerSecond = station.bitrate / 8F
|
val kiloBytesPerSecond = station.bitrate / 8F
|
||||||
val dataRateString = if (kiloBytesPerSecond >= 1000) {
|
val dataRateString = if (kiloBytesPerSecond >= 1000) {
|
||||||
String.format("%.2f mb/s", kiloBytesPerSecond / 1000F)
|
String.format(Locale.ROOT, "%.2f mb/s", kiloBytesPerSecond / 1000F)
|
||||||
} else {
|
} else {
|
||||||
String.format("%.0f kb/s", kiloBytesPerSecond)
|
String.format(Locale.ROOT, "%.0f kb/s", kiloBytesPerSecond)
|
||||||
}
|
}
|
||||||
// show the bitrate and codec if the result is available in the radio-browser.info API
|
// show the bitrate and codec if the result is available in the radio-browser.info API
|
||||||
buildString {
|
buildString {
|
||||||
@@ -190,36 +188,42 @@ data class LayoutHolder(var rootView: View) {
|
|||||||
context,
|
context,
|
||||||
sheetStreamingLinkView?.text ?: ""
|
sheetStreamingLinkView?.text ?: ""
|
||||||
)
|
)
|
||||||
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
sheetStreamingLinkView?.setOnClickListener {
|
sheetStreamingLinkView?.setOnClickListener {
|
||||||
copyToClipboard(
|
copyToClipboard(
|
||||||
context,
|
context,
|
||||||
sheetStreamingLinkView?.text ?: ""
|
sheetStreamingLinkView?.text ?: ""
|
||||||
)
|
)
|
||||||
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
sheetMetadataHistoryHeadline?.setOnClickListener {
|
sheetMetadataHistoryHeadline?.setOnClickListener {
|
||||||
copyToClipboard(
|
copyToClipboard(
|
||||||
context,
|
context,
|
||||||
sheetMetadataHistoryView?.text ?: ""
|
sheetMetadataHistoryView?.text ?: ""
|
||||||
)
|
)
|
||||||
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
sheetMetadataHistoryView?.setOnClickListener {
|
sheetMetadataHistoryView?.setOnClickListener {
|
||||||
copyToClipboard(
|
copyToClipboard(
|
||||||
context,
|
context,
|
||||||
sheetMetadataHistoryView?.text ?: ""
|
sheetMetadataHistoryView?.text ?: ""
|
||||||
)
|
)
|
||||||
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
sheetCopyMetadataButtonView?.setOnClickListener {
|
sheetCopyMetadataButtonView?.setOnClickListener {
|
||||||
copyToClipboard(
|
copyToClipboard(
|
||||||
context,
|
context,
|
||||||
sheetMetadataHistoryView?.text ?: ""
|
sheetMetadataHistoryView?.text ?: ""
|
||||||
)
|
)
|
||||||
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
sheetBitrateView?.setOnClickListener {
|
sheetBitrateView?.setOnClickListener {
|
||||||
copyToClipboard(
|
copyToClipboard(
|
||||||
context,
|
context,
|
||||||
sheetBitrateView?.text ?: ""
|
sheetBitrateView?.text ?: ""
|
||||||
)
|
)
|
||||||
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
sheetShareLinkButtonView?.setOnClickListener {
|
sheetShareLinkButtonView?.setOnClickListener {
|
||||||
val share = Intent.createChooser(Intent().apply {
|
val share = Intent.createChooser(Intent().apply {
|
||||||
@@ -238,11 +242,8 @@ data class LayoutHolder(var rootView: View) {
|
|||||||
val clip: ClipData = ClipData.newPlainText("simple text", clipString)
|
val clip: ClipData = ClipData.newPlainText("simple text", clipString)
|
||||||
val cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
cm.setPrimaryClip(clip)
|
cm.setPrimaryClip(clip)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
// since API 33 (TIRAMISU) the OS displays its own notification when content is copied to the clipboard
|
|
||||||
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Copies collected metadata to clipboard */
|
/* Copies collected metadata to clipboard */
|
||||||
@@ -251,6 +252,7 @@ data class LayoutHolder(var rootView: View) {
|
|||||||
val stringBuilder: StringBuilder = StringBuilder()
|
val stringBuilder: StringBuilder = StringBuilder()
|
||||||
metadataHistory.forEach { stringBuilder.append("${it.trim()}\n") }
|
metadataHistory.forEach { stringBuilder.append("${it.trim()}\n") }
|
||||||
copyToClipboard(rootView.context, stringBuilder.toString())
|
copyToClipboard(rootView.context, stringBuilder.toString())
|
||||||
|
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Focused (TV remote) states -->
|
||||||
<item android:color="@color/list_card_stroke_focused" android:state_focused="true" />
|
<item android:color="@color/list_card_stroke_focused" android:state_focused="true" />
|
||||||
|
<!-- Default state -->
|
||||||
<item android:color="@color/list_card_stroke_background" />
|
<item android:color="@color/list_card_stroke_background" />
|
||||||
</selector>
|
</selector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/icon_default"
|
||||||
|
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,503L400,343L560,503L720,343L760,383L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,463L240,503ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,496L720,456L560,616L400,456L240,616L200,576L200,760Q200,760 200,760Q200,760 200,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,496L200,576L200,463L200,383L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,463L200,463L200,576L200,576L200,760Q200,760 200,760Q200,760 200,760Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/icon_default"
|
||||||
|
android:pathData="M280,680L560,680L560,600L280,600L280,680ZM280,520L680,520L680,440L280,440L280,520ZM280,360L680,360L680,280L280,280L280,360ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/icon_default"
|
||||||
|
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,333L760,333L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,333ZM200,547L760,547L760,413L200,413L200,547ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,627L200,627L200,760Q200,760 200,760Q200,760 200,760ZM240,306L240,226L320,226L320,306L240,306ZM240,520L240,440L320,440L320,520L240,520ZM240,734L240,654L320,654L320,734L240,734Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/icon_default"
|
||||||
|
android:pathData="M640,800L640,520L800,520L800,800L640,800ZM400,800L400,160L560,160L560,800L400,800ZM160,800L160,360L320,360L320,800L160,800Z"/>
|
||||||
|
</vector>
|
||||||
@@ -78,6 +78,9 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:singleLine="true"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||||
android:textColor="@color/player_sheet_text_main"
|
android:textColor="@color/player_sheet_text_main"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingBottom="16dp"
|
android:paddingBottom="16dp"
|
||||||
|
android:nextFocusRight="@id/dialog_negative_button"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/guideline"
|
app:layout_constraintEnd_toStartOf="@+id/guideline"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
style="@style/Widget.Material3.Button"
|
style="@style/Widget.Material3.Button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:nextFocusLeft="@id/station_list"
|
||||||
android:text="@string/dialog_find_station_button_add" />
|
android:text="@string/dialog_find_station_button_add" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:nextFocusLeft="@id/station_list"
|
||||||
android:text="@string/dialog_generic_button_cancel" />
|
android:text="@string/dialog_generic_button_cancel" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
android:id="@+id/station_search_box_view"
|
android:id="@+id/station_search_box_view"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:clickable="true"
|
||||||
|
android:nextFocusRight="@id/dialog_negative_button"
|
||||||
|
android:nextFocusDown="@id/station_search_result_list"
|
||||||
app:iconifiedByDefault="false"
|
app:iconifiedByDefault="false"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/guideline"
|
app:layout_constraintEnd_toStartOf="@+id/guideline"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -44,6 +49,8 @@
|
|||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingBottom="16dp"
|
android:paddingBottom="16dp"
|
||||||
|
android:nextFocusRight="@id/dialog_negative_button"
|
||||||
|
android:nextFocusUp="@id/station_search_box_view"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/guideline"
|
app:layout_constraintEnd_toStartOf="@+id/guideline"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -73,6 +80,8 @@
|
|||||||
style="@style/Widget.Material3.Button"
|
style="@style/Widget.Material3.Button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:nextFocusUp="@id/station_search_box_view"
|
||||||
|
android:nextFocusLeft="@id/station_search_result_list"
|
||||||
android:text="@string/dialog_find_station_button_add" />
|
android:text="@string/dialog_find_station_button_add" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -81,6 +90,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:nextFocusLeft="@id/station_search_result_list"
|
||||||
android:text="@string/dialog_generic_button_cancel" />
|
android:text="@string/dialog_generic_button_cancel" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dialog_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/pref_language_selection_title"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingBottom="16dp" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scrollbars="vertical">
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/language_radio_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:nextFocusRight="@+id/dialog_positive_button"
|
android:nextFocusRight="@+id/dialog_negative_button"
|
||||||
android:background="@drawable/selector_search_result_item">
|
android:background="@drawable/selector_search_result_item">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
|
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
|
||||||
android:textColor="@color/text_default"
|
android:textColor="@color/text_default"
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||||
android:textColor="@color/text_lightweight"
|
android:textColor="@color/text_lightweight"
|
||||||
|
|||||||
@@ -72,10 +72,11 @@
|
|||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/player_station_metadata"
|
android:id="@+id/player_station_metadata"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="marquee"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||||
|
|||||||
@@ -56,7 +56,8 @@
|
|||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:letterSpacing="0"
|
android:letterSpacing="0"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
|
|||||||
@@ -22,6 +22,24 @@
|
|||||||
app:strokeColor="@color/list_card_stroke_background"
|
app:strokeColor="@color/list_card_stroke_background"
|
||||||
app:strokeWidth="3dp" />
|
app:strokeWidth="3dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/card_visualizer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginEnd="70dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:focusable="true"
|
||||||
|
android:clickable="true"
|
||||||
|
android:stateListAnimator="@null"
|
||||||
|
app:backgroundTint="@color/list_card_background"
|
||||||
|
app:icon="@drawable/ic_visualizer_24dp"
|
||||||
|
app:iconTint="@color/icon_default"
|
||||||
|
app:rippleColor="@color/list_card_stroke_background"
|
||||||
|
app:strokeColor="@color/list_card_stroke_background"
|
||||||
|
app:strokeWidth="3dp" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
android:id="@+id/card_settings"
|
android:id="@+id/card_settings"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -61,6 +61,17 @@
|
|||||||
app:shapeAppearanceOverlay="@style/RoundedCorners"
|
app:shapeAppearanceOverlay="@style/RoundedCorners"
|
||||||
app:srcCompat="@drawable/ic_image_white_36dp" />
|
app:srcCompat="@drawable/ic_image_white_36dp" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/reorder_checkbox"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:foregroundTint="@color/icon_default"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/station_icon"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/starred_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/station_icon"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/starred_icon"
|
android:id="@+id/starred_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -86,7 +97,7 @@
|
|||||||
android:textColor="@color/text_lightweight"
|
android:textColor="@color/text_lightweight"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/station_icon"
|
app:layout_constraintBottom_toBottomOf="@+id/station_icon"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/playback_button"
|
app:layout_constraintEnd_toStartOf="@+id/playback_button"
|
||||||
app:layout_constraintStart_toEndOf="@+id/starred_icon"
|
app:layout_constraintStart_toEndOf="@+id/reorder_checkbox"
|
||||||
app:layout_constraintTop_toTopOf="@+id/station_icon"
|
app:layout_constraintTop_toTopOf="@+id/station_icon"
|
||||||
tools:text="@string/sample_text_station_name" />
|
tools:text="@string/sample_text_station_name" />
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dialog_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/pref_language_selection_title"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingBottom="12dp" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scrollbars="vertical">
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/language_radio_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -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_banner_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -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_channel_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_channel_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -13,6 +13,12 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_map_fragment_to_settings_fragment"
|
android:id="@+id/action_map_fragment_to_settings_fragment"
|
||||||
app:destination="@id/settings_destination" />
|
app:destination="@id/settings_destination" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_map_fragment_to_visualizer_fragment"
|
||||||
|
app:destination="@id/visualizer_destination" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_map_fragment_to_player_to_add_station"
|
||||||
|
app:destination="@id/add_station_destination" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<!-- SETTINGS -->
|
<!-- SETTINGS -->
|
||||||
@@ -39,4 +45,11 @@
|
|||||||
android:id="@+id/visualizer_destination"
|
android:id="@+id/visualizer_destination"
|
||||||
android:name="com.michatec.radio.VisualizerFragment"
|
android:name="com.michatec.radio.VisualizerFragment"
|
||||||
android:label="Visualizer" />
|
android:label="Visualizer" />
|
||||||
|
|
||||||
|
<!-- ADD STATION (TV) -->
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/add_station_destination"
|
||||||
|
android:name="com.michatec.radio.AddStationFragment"
|
||||||
|
android:label="Add Station"
|
||||||
|
tools:layout="@layout/dialog_find_station" />
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<string name="pref_buffer_size_title">Brug større buffer</string>
|
<string name="pref_buffer_size_title">Brug større buffer</string>
|
||||||
<string name="pref_edit_station_stream_summary_disabled">Redigering af stream-links er deaktiveret.</string>
|
<string name="pref_edit_station_stream_summary_disabled">Redigering af stream-links er deaktiveret.</string>
|
||||||
<string name="pref_edit_station_stream_summary_enabled">Redigering af stream-links er aktiveret. Sørg for at angive en korrekt streamadresse.</string>
|
<string name="pref_edit_station_stream_summary_enabled">Redigering af stream-links er aktiveret. Sørg for at angive en korrekt streamadresse.</string>
|
||||||
|
<string name="pref_edit_station_stream_title">Rediger stream-links</string>
|
||||||
<string name="pref_edit_station_summary_disabled">Redigering af stationsoplysninger er deaktiveret.</string>
|
<string name="pref_edit_station_summary_disabled">Redigering af stationsoplysninger er deaktiveret.</string>
|
||||||
<string name="pref_edit_station_summary_enabled">Redigering er aktiveret. Langt tryk for at redigere.</string>
|
<string name="pref_edit_station_summary_enabled">Redigering er aktiveret. Langt tryk for at redigere.</string>
|
||||||
<string name="pref_edit_station_title">Rediger station</string>
|
<string name="pref_edit_station_title">Rediger station</string>
|
||||||
@@ -110,4 +111,45 @@
|
|||||||
<!-- Snackbars -->
|
<!-- Snackbars -->
|
||||||
<string name="snackbar_show">Vis</string>
|
<string name="snackbar_show">Vis</string>
|
||||||
<string name="snackbar_update_available">er tilgængelig!</string>
|
<string name="snackbar_update_available">er tilgængelig!</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Sprog</string>
|
||||||
|
<string name="pref_language_selection_summary">Aktuelt sprog</string>
|
||||||
|
<string name="pref_language_system">🗺️ System</string>
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_update_collection_title">Opdater stationer</string>
|
||||||
|
<string name="pref_update_collection_summary">Download den nyeste version af alle stationer.</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">Download den nyeste version af alle stationer?</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">Opdater</string>
|
||||||
|
<string name="pref_audio_effects_title">Lydeffekter</string>
|
||||||
|
<string name="pref_bass_boost_title">Bas-forstærkning</string>
|
||||||
|
<string name="pref_bass_boost_summary">Øg basforstærkningen.</string>
|
||||||
|
<string name="pref_reverb_title">Hall</string>
|
||||||
|
<string name="pref_reverb_summary">Juster hall-blanding.</string>
|
||||||
|
<string name="pref_drc_title">Dynamisk rækkeviddekomprimering</string>
|
||||||
|
<string name="pref_drc_summary">Komprimer det dynamiske område for konsistent lydstyrke.</string>
|
||||||
|
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
|
||||||
|
<string name="pref_eq_mid_title">Equalizer: 125 Hz</string>
|
||||||
|
<string name="pref_eq_high_title">Equalizer: 4 kHz</string>
|
||||||
|
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
|
||||||
|
<string name="pref_eq_band_2_title">Equalizer: 250 Hz</string>
|
||||||
|
<string name="pref_eq_band_3_title">Equalizer: 500 Hz</string>
|
||||||
|
<string name="pref_eq_band_4_title">Equalizer: 1 kHz</string>
|
||||||
|
<string name="pref_eq_band_5_title">Equalizer: 2 kHz</string>
|
||||||
|
<string name="pref_eq_band_6_title">Equalizer: 8 kHz</string>
|
||||||
|
<string name="pref_eq_band_7_title">Equalizer: 16 kHz</string>
|
||||||
|
<string name="pref_equalizer_title">Equalizer</string>
|
||||||
|
<string name="pref_equalizer_summary">Juster lydfrekvenserne.</string>
|
||||||
|
<string name="pref_equalizer_summary_off">Justering af lydfrekvenserne er deaktiveret.</string>
|
||||||
|
<string name="pref_equalizer_reset_title">Nulstil equalizer</string>
|
||||||
|
<string name="pref_preset_selection_title">Vælg forudindstilling</string>
|
||||||
|
<string name="pref_preset_selection_summary">Vælg en lydforudindstilling</string>
|
||||||
|
<string name="pref_preset_none">Ingen (Manuel)</string>
|
||||||
|
<string name="pref_preset_rock">Rock</string>
|
||||||
|
<string name="pref_preset_pop">Pop</string>
|
||||||
|
<string name="pref_preset_jazz">Jazz</string>
|
||||||
|
<string name="pref_preset_flat">Flad</string>
|
||||||
|
<string name="loading">Indlæser…</string>
|
||||||
|
<string name="media_route_menu_title">Cast</string>
|
||||||
|
<string name="pref_visualizer_title">Spektrumanalysator</string>
|
||||||
|
<string name="pref_visualizer_summary">Vis spektrumanalysatoren.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -69,6 +69,11 @@
|
|||||||
<string name="pref_edit_station_summary_enabled">Die Bearbeitung von Senderinformationen ist aktiviert. Drücke lange, um den Bearbeitungsmodus aufzurufen.</string>
|
<string name="pref_edit_station_summary_enabled">Die Bearbeitung von Senderinformationen ist aktiviert. Drücke lange, um den Bearbeitungsmodus aufzurufen.</string>
|
||||||
<string name="pref_edit_station_title">Sender bearbeiten</string>
|
<string name="pref_edit_station_title">Sender bearbeiten</string>
|
||||||
<string name="pref_general_title">Allgemein</string>
|
<string name="pref_general_title">Allgemein</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Sprache</string>
|
||||||
|
<string name="pref_language_selection_summary">Aktuelle Sprache</string>
|
||||||
|
<string name="pref_language_system">🗺️ System</string>
|
||||||
|
<string name="pref_language_en" translatable="false">🇬🇧 Englisch</string>
|
||||||
<string name="pref_license_title">Diese Anwendung ist Open Source</string>
|
<string name="pref_license_title">Diese Anwendung ist Open Source</string>
|
||||||
<string name="pref_license_summary">Lizenziert unter der GPLv3 License</string>
|
<string name="pref_license_summary">Lizenziert unter der GPLv3 License</string>
|
||||||
<string name="pref_links_title">Links</string>
|
<string name="pref_links_title">Links</string>
|
||||||
@@ -145,6 +150,8 @@
|
|||||||
<string name="pref_preset_pop">Pop</string>
|
<string name="pref_preset_pop">Pop</string>
|
||||||
<string name="pref_preset_jazz">Jazz</string>
|
<string name="pref_preset_jazz">Jazz</string>
|
||||||
<string name="pref_preset_flat">Flach</string>
|
<string name="pref_preset_flat">Flach</string>
|
||||||
|
<string name="media_route_menu_title">Streamen</string>
|
||||||
<string name="pref_visualizer_title">Spektrumanzeige</string>
|
<string name="pref_visualizer_title">Spektrumanzeige</string>
|
||||||
<string name="pref_visualizer_summary">Sehe die Spektrumanzeige.</string>
|
<string name="pref_visualizer_summary">Sehe die Spektrumanzeige.</string>
|
||||||
|
<string name="loading">Lade…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<string name="descr_player_playback_button">Αναπαραγωγή/Παύση</string>
|
<string name="descr_player_playback_button">Αναπαραγωγή/Παύση</string>
|
||||||
<string name="descr_player_station_image">Εικόνα σταθμού</string>
|
<string name="descr_player_station_image">Εικόνα σταθμού</string>
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
|
<string name="dialog_add_station_title">Προσθήκη Σταθμού</string>
|
||||||
<string name="dialog_edit_station_name">Όνομα σταθμού</string>
|
<string name="dialog_edit_station_name">Όνομα σταθμού</string>
|
||||||
<string name="dialog_edit_stream_uri">Διεύθυνση ροής</string>
|
<string name="dialog_edit_stream_uri">Διεύθυνση ροής</string>
|
||||||
<string name="dialog_error_message_default">Προέκυψε ένα σφάλμα</string>
|
<string name="dialog_error_message_default">Προέκυψε ένα σφάλμα</string>
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<string name="dialog_generic_button_okay">ΟΚ</string>
|
<string name="dialog_generic_button_okay">ΟΚ</string>
|
||||||
<string name="dialog_generic_details_button">Εμφάνιση λεπτομερειών</string>
|
<string name="dialog_generic_details_button">Εμφάνιση λεπτομερειών</string>
|
||||||
<string name="dialog_opml_import_details_default">Δεν υπάρχουν διαθέσιμες λεπτομέρειες</string>
|
<string name="dialog_opml_import_details_default">Δεν υπάρχουν διαθέσιμες λεπτομέρειες</string>
|
||||||
|
<string name="dialog_restore_collection_replace_existing">Αντικατάσταση της τρέχουσας συλλογής ραδιοφωνικών σταθμών με τον ραδιοφωνικό σταθμό από το αντίγραφο ασφαλείας;</string>
|
||||||
<string name="dialog_yes_no_message_remove_station">Αφαίρεση αυτού του σταθμού;</string>
|
<string name="dialog_yes_no_message_remove_station">Αφαίρεση αυτού του σταθμού;</string>
|
||||||
<string name="dialog_yes_no_message_update_station_images">Να κατεβεί η τελευταία έκδοση όλων των εικόνων σταθμών;</string>
|
<string name="dialog_yes_no_message_update_station_images">Να κατεβεί η τελευταία έκδοση όλων των εικόνων σταθμών;</string>
|
||||||
<string name="dialog_yes_no_positive_button_default">Ναι</string>
|
<string name="dialog_yes_no_positive_button_default">Ναι</string>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
<string name="notification_skip_to_previous">Προηγούμενο</string>
|
<string name="notification_skip_to_previous">Προηγούμενο</string>
|
||||||
<string name="notification_skip_to_next">Επόμενο</string>
|
<string name="notification_skip_to_next">Επόμενο</string>
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
|
<string name="onboarding_app_description">Βυθιστείτε στον ήχο της επιλογής σας!</string>
|
||||||
<string name="onboarding_app_get_started">Ας ξεκινήσουμε</string>
|
<string name="onboarding_app_get_started">Ας ξεκινήσουμε</string>
|
||||||
<!-- Player -->
|
<!-- Player -->
|
||||||
<string name="player_sheet_h2_station_metadata">Παίζεται τώρα</string>
|
<string name="player_sheet_h2_station_metadata">Παίζεται τώρα</string>
|
||||||
@@ -91,6 +94,7 @@
|
|||||||
<!-- Toasts -->
|
<!-- Toasts -->
|
||||||
<string name="toastmessage_backed_up">έχει δημιουργηθεί επιτυχώς το αντίγραφο ασφαλείας.</string>
|
<string name="toastmessage_backed_up">έχει δημιουργηθεί επιτυχώς το αντίγραφο ασφαλείας.</string>
|
||||||
<string name="toastmessage_copied_to_clipboard">Αντιγράφηκε στο πρόχειρο.</string>
|
<string name="toastmessage_copied_to_clipboard">Αντιγράφηκε στο πρόχειρο.</string>
|
||||||
|
<string name="toastmessage_connection_failed">Η σύνδεση δεν μπόρεσε να δημιουργηθεί ή να επαναφερθεί.</string>
|
||||||
<string name="toastmessage_error_download_error">Σφάλμα λήψης</string>
|
<string name="toastmessage_error_download_error">Σφάλμα λήψης</string>
|
||||||
<string name="toastmessage_error_restart_playback_failed">Αδυναμία εκκίνησης ή επανεκκίνησης της αναπαραγωγής.</string>
|
<string name="toastmessage_error_restart_playback_failed">Αδυναμία εκκίνησης ή επανεκκίνησης της αναπαραγωγής.</string>
|
||||||
<string name="toastmessage_install_file_helper">Παρακαλώ εγκαταστήστε ένα πρόγραμμα διαχείρισης αρχείων.</string>
|
<string name="toastmessage_install_file_helper">Παρακαλώ εγκαταστήστε ένα πρόγραμμα διαχείρισης αρχείων.</string>
|
||||||
@@ -109,4 +113,45 @@
|
|||||||
<!-- Snackbars -->
|
<!-- Snackbars -->
|
||||||
<string name="snackbar_show">Εμφάνισε</string>
|
<string name="snackbar_show">Εμφάνισε</string>
|
||||||
<string name="snackbar_update_available">είναι διαθέσιμη!</string>
|
<string name="snackbar_update_available">είναι διαθέσιμη!</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Γλώσσα</string>
|
||||||
|
<string name="pref_language_selection_summary">Τρέχουσα γλώσσα</string>
|
||||||
|
<string name="pref_language_system">🗺️ Σύστημα</string>
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_update_collection_title">Ενημέρωση Σταθμών</string>
|
||||||
|
<string name="pref_update_collection_summary">Κατεβάστε την τελευταία έκδοση όλων των σταθμών.</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">Κατεβάστε την τελευταία έκδοση όλων των σταθμών;</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">Ενημέρωση</string>
|
||||||
|
<string name="pref_audio_effects_title">Ηχητικά Εφέ</string>
|
||||||
|
<string name="pref_bass_boost_title">Ενίσχυση Μπάσων</string>
|
||||||
|
<string name="pref_bass_boost_summary">Αύξηση της ενίσχυσης μπάσων.</string>
|
||||||
|
<string name="pref_reverb_title">Αντήχηση</string>
|
||||||
|
<string name="pref_reverb_summary">Προσαρμογή μίξης αντήχησης.</string>
|
||||||
|
<string name="pref_drc_title">Συμπίεση Δυναμικού Εύρους</string>
|
||||||
|
<string name="pref_drc_summary">Συμπίεση δυναμικού εύρους για σταθερή ένταση.</string>
|
||||||
|
<string name="pref_eq_low_title">Ισοσταθμιστής: 31 Hz</string>
|
||||||
|
<string name="pref_eq_mid_title">Ισοσταθμιστής: 125 Hz</string>
|
||||||
|
<string name="pref_eq_high_title">Ισοσταθμιστής: 4 kHz</string>
|
||||||
|
<string name="pref_eq_band_1_title">Ισοσταθμιστής: 62 Hz</string>
|
||||||
|
<string name="pref_eq_band_2_title">Ισοσταθμιστής: 250 Hz</string>
|
||||||
|
<string name="pref_eq_band_3_title">Ισοσταθμιστής: 500 Hz</string>
|
||||||
|
<string name="pref_eq_band_4_title">Ισοσταθμιστής: 1 kHz</string>
|
||||||
|
<string name="pref_eq_band_5_title">Ισοσταθμιστής: 2 kHz</string>
|
||||||
|
<string name="pref_eq_band_6_title">Ισοσταθμιστής: 8 kHz</string>
|
||||||
|
<string name="pref_eq_band_7_title">Ισοσταθμιστής: 16 kHz</string>
|
||||||
|
<string name="pref_equalizer_title">Ισοσταθμιστής</string>
|
||||||
|
<string name="pref_equalizer_summary">Προσαρμογή ηχητικών συχνοτήτων.</string>
|
||||||
|
<string name="pref_equalizer_summary_off">Η προσαρμογή ηχητικών συχνοτήτων είναι απενεργοποιημένη.</string>
|
||||||
|
<string name="pref_equalizer_reset_title">Επαναφορά Ισοσταθμιστή</string>
|
||||||
|
<string name="pref_preset_selection_title">Επιλογή Προκαθορισμένου</string>
|
||||||
|
<string name="pref_preset_selection_summary">Επιλέξτε ένα ηχητικό προκαθορισμένο.</string>
|
||||||
|
<string name="pref_preset_none">Κανένα (Χειροκίνητο)</string>
|
||||||
|
<string name="pref_preset_rock">Rock</string>
|
||||||
|
<string name="pref_preset_pop">Pop</string>
|
||||||
|
<string name="pref_preset_jazz">Jazz</string>
|
||||||
|
<string name="pref_preset_flat">Επίπεδο</string>
|
||||||
|
<string name="loading">Φόρτωση…</string>
|
||||||
|
<string name="media_route_menu_title">Μετάδοση</string>
|
||||||
|
<string name="pref_visualizer_title">Αναλυτής Φάσματος</string>
|
||||||
|
<string name="pref_visualizer_summary">Εμφάνιση του Αναλυτή Φάσματος.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<string name="pref_buffer_size_title">Utiliser un tampon plus grand</string>
|
<string name="pref_buffer_size_title">Utiliser un tampon plus grand</string>
|
||||||
<string name="pref_edit_station_stream_summary_disabled">La modification des liens de streaming est désactivée.</string>
|
<string name="pref_edit_station_stream_summary_disabled">La modification des liens de streaming est désactivée.</string>
|
||||||
<string name="pref_edit_station_stream_summary_enabled">La modification des liens de streaming est activée. Assurez-vous dentrer une adresse de flux correcte.</string>
|
<string name="pref_edit_station_stream_summary_enabled">La modification des liens de streaming est activée. Assurez-vous dentrer une adresse de flux correcte.</string>
|
||||||
|
<string name="pref_edit_station_stream_title">Modifier les liens de streaming</string>
|
||||||
<string name="pref_edit_station_summary_disabled">La modification des informations de la station est désactivée.</string>
|
<string name="pref_edit_station_summary_disabled">La modification des informations de la station est désactivée.</string>
|
||||||
<string name="pref_edit_station_summary_enabled">La modification est activée. Maintenez appuyé pour éditer.</string>
|
<string name="pref_edit_station_summary_enabled">La modification est activée. Maintenez appuyé pour éditer.</string>
|
||||||
<string name="pref_edit_station_title">Modifier la station</string>
|
<string name="pref_edit_station_title">Modifier la station</string>
|
||||||
@@ -110,4 +111,45 @@
|
|||||||
<!-- Snackbars -->
|
<!-- Snackbars -->
|
||||||
<string name="snackbar_show">Afficher</string>
|
<string name="snackbar_show">Afficher</string>
|
||||||
<string name="snackbar_update_available">est disponible !</string>
|
<string name="snackbar_update_available">est disponible !</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Langue</string>
|
||||||
|
<string name="pref_language_selection_summary">Langue actuelle</string>
|
||||||
|
<string name="pref_language_system">🗺️ Système</string>
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_update_collection_title">Mettre à jour les stations</string>
|
||||||
|
<string name="pref_update_collection_summary">Téléchargez la dernière version de toutes les stations.</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">Téléchargez la dernière version de toutes les stations ?</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">Mettre à jour</string>
|
||||||
|
<string name="pref_audio_effects_title">Effets Audio</string>
|
||||||
|
<string name="pref_bass_boost_title">Amplification des basses</string>
|
||||||
|
<string name="pref_bass_boost_summary">Augmenter l amplification des basses.</string>
|
||||||
|
<string name="pref_reverb_title">Réverbération</string>
|
||||||
|
<string name="pref_reverb_summary">Ajuster le mix de réverbération.</string>
|
||||||
|
<string name="pref_drc_title">Compression Dynamique</string>
|
||||||
|
<string name="pref_drc_summary">Compresser la plage dynamique pour un volume constant.</string>
|
||||||
|
<string name="pref_eq_low_title">Égaliseur : 31 Hz</string>
|
||||||
|
<string name="pref_eq_mid_title">Égaliseur : 125 Hz</string>
|
||||||
|
<string name="pref_eq_high_title">Égaliseur : 4 kHz</string>
|
||||||
|
<string name="pref_eq_band_1_title">Égaliseur : 62 Hz</string>
|
||||||
|
<string name="pref_eq_band_2_title">Égaliseur : 250 Hz</string>
|
||||||
|
<string name="pref_eq_band_3_title">Égaliseur : 500 Hz</string>
|
||||||
|
<string name="pref_eq_band_4_title">Égaliseur : 1 kHz</string>
|
||||||
|
<string name="pref_eq_band_5_title">Égaliseur : 2 kHz</string>
|
||||||
|
<string name="pref_eq_band_6_title">Égaliseur : 8 kHz</string>
|
||||||
|
<string name="pref_eq_band_7_title">Égaliseur : 16 kHz</string>
|
||||||
|
<string name="pref_equalizer_title">Égaliseur</string>
|
||||||
|
<string name="pref_equalizer_summary">Ajuster les fréquences audio.</string>
|
||||||
|
<string name="pref_equalizer_summary_off">L ajustement des fréquences audio est désactivé.</string>
|
||||||
|
<string name="pref_equalizer_reset_title">Réinitialiser l égaliseur</string>
|
||||||
|
<string name="pref_preset_selection_title">Sélectionner un préréglage</string>
|
||||||
|
<string name="pref_preset_selection_summary">Choisissez un préréglage audio.</string>
|
||||||
|
<string name="pref_preset_none">Aucun (Manuel)</string>
|
||||||
|
<string name="pref_preset_rock">Rock</string>
|
||||||
|
<string name="pref_preset_pop">Pop</string>
|
||||||
|
<string name="pref_preset_jazz">Jazz</string>
|
||||||
|
<string name="pref_preset_flat">Plat</string>
|
||||||
|
<string name="loading">Chargement…</string>
|
||||||
|
<string name="media_route_menu_title">Diffuser</string>
|
||||||
|
<string name="pref_visualizer_title">Analyseur de spectre</string>
|
||||||
|
<string name="pref_visualizer_summary">Afficher l analyseur de spectre.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<string name="pref_buffer_size_title">大きなバッファを使用</string>
|
<string name="pref_buffer_size_title">大きなバッファを使用</string>
|
||||||
<string name="pref_edit_station_stream_summary_disabled">ストリームリンクの編集は無効です。</string>
|
<string name="pref_edit_station_stream_summary_disabled">ストリームリンクの編集は無効です。</string>
|
||||||
<string name="pref_edit_station_stream_summary_enabled">ストリームリンクの編集は有効です。正しいURLを入力してください。</string>
|
<string name="pref_edit_station_stream_summary_enabled">ストリームリンクの編集は有効です。正しいURLを入力してください。</string>
|
||||||
|
<string name="pref_edit_station_stream_title">ストリームリンクを編集</string>
|
||||||
<string name="pref_edit_station_summary_disabled">局情報の編集は無効です。</string>
|
<string name="pref_edit_station_summary_disabled">局情報の編集は無効です。</string>
|
||||||
<string name="pref_edit_station_summary_enabled">局情報の編集は有効です。長押しで編集モードに入ります。</string>
|
<string name="pref_edit_station_summary_enabled">局情報の編集は有効です。長押しで編集モードに入ります。</string>
|
||||||
<string name="pref_edit_station_title">局を編集</string>
|
<string name="pref_edit_station_title">局を編集</string>
|
||||||
@@ -111,4 +112,45 @@
|
|||||||
<!-- スナックバー -->
|
<!-- スナックバー -->
|
||||||
<string name="snackbar_show">表示</string>
|
<string name="snackbar_show">表示</string>
|
||||||
<string name="snackbar_update_available">が利用可能です!</string>
|
<string name="snackbar_update_available">が利用可能です!</string>
|
||||||
|
<!-- 言語選択 -->
|
||||||
|
<string name="pref_language_selection_title">言語</string>
|
||||||
|
<string name="pref_language_selection_summary">現在の言語</string>
|
||||||
|
<string name="pref_language_system">🗺️ システム</string>
|
||||||
|
<!-- 設定 -->
|
||||||
|
<string name="pref_update_collection_title">局を更新</string>
|
||||||
|
<string name="pref_update_collection_summary">すべての局の最新バージョンをダウンロードします。</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">すべての局の最新バージョンをダウンロードしますか?</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">更新</string>
|
||||||
|
<string name="pref_audio_effects_title">オーディオエフェクト</string>
|
||||||
|
<string name="pref_bass_boost_title">バスブースト</string>
|
||||||
|
<string name="pref_bass_boost_summary">低音を増強します。</string>
|
||||||
|
<string name="pref_reverb_title">リバーブ</string>
|
||||||
|
<string name="pref_reverb_summary">リバーブミスを調整します。</string>
|
||||||
|
<string name="pref_drc_title">ダイナミックレンジ圧縮</string>
|
||||||
|
<string name="pref_drc_summary">音量的一定のためダイナミックレンジを圧縮します。</string>
|
||||||
|
<string name="pref_eq_low_title">イコライザー:31 Hz</string>
|
||||||
|
<string name="pref_eq_mid_title">イコライザー:125 Hz</string>
|
||||||
|
<string name="pref_eq_high_title">イコライザー:4 kHz</string>
|
||||||
|
<string name="pref_eq_band_1_title">イコライザー:62 Hz</string>
|
||||||
|
<string name="pref_eq_band_2_title">イコライザー:250 Hz</string>
|
||||||
|
<string name="pref_eq_band_3_title">イコライザー:500 Hz</string>
|
||||||
|
<string name="pref_eq_band_4_title">イコライザー:1 kHz</string>
|
||||||
|
<string name="pref_eq_band_5_title">イコライザー:2 kHz</string>
|
||||||
|
<string name="pref_eq_band_6_title">イコライザー:8 kHz</string>
|
||||||
|
<string name="pref_eq_band_7_title">イコライザー:16 kHz</string>
|
||||||
|
<string name="pref_equalizer_title">イコライザー</string>
|
||||||
|
<string name="pref_equalizer_summary">オーディオ周波数を調整します。</string>
|
||||||
|
<string name="pref_equalizer_summary_off">オーディオ周波数の調整は無効です。</string>
|
||||||
|
<string name="pref_equalizer_reset_title">イコライザーリセット</string>
|
||||||
|
<string name="pref_preset_selection_title">プリセットを選択</string>
|
||||||
|
<string name="pref_preset_selection_summary">オーディオプリセットを選択</string>
|
||||||
|
<string name="pref_preset_none">なし(マニュアル)</string>
|
||||||
|
<string name="pref_preset_rock">ロック</string>
|
||||||
|
<string name="pref_preset_pop">ポップ</string>
|
||||||
|
<string name="pref_preset_jazz">ジャズ</string>
|
||||||
|
<string name="pref_preset_flat">フラット</string>
|
||||||
|
<string name="loading">読み込み中…</string>
|
||||||
|
<string name="media_route_menu_title">キャスト</string>
|
||||||
|
<string name="pref_visualizer_title">スペクトラムアナライザー</string>
|
||||||
|
<string name="pref_visualizer_summary">スペクトラムアナライザーを表示します。</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<string name="descr_player_playback_button">Afspelen/pauzeren</string>
|
<string name="descr_player_playback_button">Afspelen/pauzeren</string>
|
||||||
<string name="descr_player_station_image">Zenderafbeelding</string>
|
<string name="descr_player_station_image">Zenderafbeelding</string>
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
|
<string name="dialog_add_station_title">Zender Toevoegen</string>
|
||||||
<string name="dialog_edit_station_name">Zendernaam</string>
|
<string name="dialog_edit_station_name">Zendernaam</string>
|
||||||
<string name="dialog_edit_stream_uri">Stream adres</string>
|
<string name="dialog_edit_stream_uri">Stream adres</string>
|
||||||
<string name="dialog_error_message_default">Er is een fout opgetreden</string>
|
<string name="dialog_error_message_default">Er is een fout opgetreden</string>
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<string name="dialog_generic_button_okay">OK</string>
|
<string name="dialog_generic_button_okay">OK</string>
|
||||||
<string name="dialog_generic_details_button">Toon details</string>
|
<string name="dialog_generic_details_button">Toon details</string>
|
||||||
<string name="dialog_opml_import_details_default">Geen details beschikbaar</string>
|
<string name="dialog_opml_import_details_default">Geen details beschikbaar</string>
|
||||||
|
<string name="dialog_restore_collection_replace_existing">Huidige collectie van radiozenders vervangen met de radiozender uit de back-up?</string>
|
||||||
<string name="dialog_yes_no_message_remove_station">Verwijder deze zender?</string>
|
<string name="dialog_yes_no_message_remove_station">Verwijder deze zender?</string>
|
||||||
<string name="dialog_yes_no_message_update_station_images">Laatste versie van alle zenderafbeeldingen downloaden?</string>
|
<string name="dialog_yes_no_message_update_station_images">Laatste versie van alle zenderafbeeldingen downloaden?</string>
|
||||||
<string name="dialog_yes_no_positive_button_default">Ja</string>
|
<string name="dialog_yes_no_positive_button_default">Ja</string>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
<string name="notification_skip_to_previous">Vorige</string>
|
<string name="notification_skip_to_previous">Vorige</string>
|
||||||
<string name="notification_skip_to_next">Volgende</string>
|
<string name="notification_skip_to_next">Volgende</string>
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
|
<string name="onboarding_app_description">Dompel jezelf onder in het geluid van je keuze!</string>
|
||||||
<string name="onboarding_app_get_started">Aan de slag</string>
|
<string name="onboarding_app_get_started">Aan de slag</string>
|
||||||
<!-- Player -->
|
<!-- Player -->
|
||||||
<string name="player_sheet_h2_station_metadata">Nu aan het afspelen</string>
|
<string name="player_sheet_h2_station_metadata">Nu aan het afspelen</string>
|
||||||
@@ -64,6 +67,11 @@
|
|||||||
<string name="pref_general_title">Algemeen</string>
|
<string name="pref_general_title">Algemeen</string>
|
||||||
<string name="pref_m3u_export_summary">Sla je radiozenders op in een M3U afspeellijstbestand dat in andere spelers kan worden geïmporteerd.</string>
|
<string name="pref_m3u_export_summary">Sla je radiozenders op in een M3U afspeellijstbestand dat in andere spelers kan worden geïmporteerd.</string>
|
||||||
<string name="pref_m3u_export_title">Exporteer M3U</string>
|
<string name="pref_m3u_export_title">Exporteer M3U</string>
|
||||||
|
<string name="pref_pls_export_summary">Sla je radiozenders op in een PLS afspeellijstbestand dat in andere spelers kan worden geïmporteerd.</string>
|
||||||
|
<string name="pref_pls_export_title">Exporteer PLS</string>
|
||||||
|
<string name="pref_license_title">Deze applicatie is open source</string>
|
||||||
|
<string name="pref_license_summary">Gelicentieerd onder de GPLv3 licentie</string>
|
||||||
|
<string name="pref_links_title">Links</string>
|
||||||
<string name="pref_maintenance_title">Onderhoud</string>
|
<string name="pref_maintenance_title">Onderhoud</string>
|
||||||
<string name="pref_station_export_summary">Sla de gehele collectie radiozenders, inclusief afbeeldingen, in de apparaatopslag op.</string>
|
<string name="pref_station_export_summary">Sla de gehele collectie radiozenders, inclusief afbeeldingen, in de apparaatopslag op.</string>
|
||||||
<string name="pref_station_export_title">Exporteer Zender</string>
|
<string name="pref_station_export_title">Exporteer Zender</string>
|
||||||
@@ -86,6 +94,12 @@
|
|||||||
<!-- Toasts -->
|
<!-- Toasts -->
|
||||||
<string name="toastmessage_backed_up">back-up is succesvol gemaakt.</string>
|
<string name="toastmessage_backed_up">back-up is succesvol gemaakt.</string>
|
||||||
<string name="toastmessage_copied_to_clipboard">Gekopieerd naar het klembord.</string>
|
<string name="toastmessage_copied_to_clipboard">Gekopieerd naar het klembord.</string>
|
||||||
|
<string name="toastmessage_connection_failed">De verbinding kon niet tot stand worden gebracht of hersteld.</string>
|
||||||
|
<string name="toastmessage_error_restart_playback_failed">Kan afspelen niet starten of herstarten.</string>
|
||||||
|
<string name="toastmessage_preview_playback_started">Voorbeeld-afspelen is gestart.</string>
|
||||||
|
<string name="toastmessage_preview_playback_failed">Kan voorbeeld niet afspelen.</string>
|
||||||
|
<string name="toastmessage_save_pls">Radiozenders opslaan als PLS…</string>
|
||||||
|
<string name="toastmessage_station_duplicate">Deze zender is een duplicaat.</string>
|
||||||
<string name="toastmessage_error_download_error">Download fout</string>
|
<string name="toastmessage_error_download_error">Download fout</string>
|
||||||
<string name="toastmessage_install_file_helper">Installeer alstublieft een bestandsbeheer applicatie.</string>
|
<string name="toastmessage_install_file_helper">Installeer alstublieft een bestandsbeheer applicatie.</string>
|
||||||
<string name="toastmessage_restored">Zenders zijn succesvol hersteld.</string>
|
<string name="toastmessage_restored">Zenders zijn succesvol hersteld.</string>
|
||||||
@@ -99,4 +113,45 @@
|
|||||||
<!-- Snackbars -->
|
<!-- Snackbars -->
|
||||||
<string name="snackbar_show">Weergeven</string>
|
<string name="snackbar_show">Weergeven</string>
|
||||||
<string name="snackbar_update_available">is beschikbaar!</string>
|
<string name="snackbar_update_available">is beschikbaar!</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Taal</string>
|
||||||
|
<string name="pref_language_selection_summary">Huidige taal</string>
|
||||||
|
<string name="pref_language_system">🗺️ Systeem</string>
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_update_collection_title">Zenders Bijwerken</string>
|
||||||
|
<string name="pref_update_collection_summary">Download de laatste versie van alle zenders.</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">Download de laatste versie van alle zenders?</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">Bijwerken</string>
|
||||||
|
<string name="pref_audio_effects_title">Audio Effecten</string>
|
||||||
|
<string name="pref_bass_boost_title">Bass Boost</string>
|
||||||
|
<string name="pref_bass_boost_summary">Verhoog de bassversterking.</string>
|
||||||
|
<string name="pref_reverb_title">Reverb</string>
|
||||||
|
<string name="pref_reverb_summary">Pas de reverb mix aan.</string>
|
||||||
|
<string name="pref_drc_title">Dynamisch Bereik Compressie</string>
|
||||||
|
<string name="pref_drc_summary">Comprimeer het dynamisch bereik voor consistent volume.</string>
|
||||||
|
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
|
||||||
|
<string name="pref_eq_mid_title">Equalizer: 125 Hz</string>
|
||||||
|
<string name="pref_eq_high_title">Equalizer: 4 kHz</string>
|
||||||
|
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
|
||||||
|
<string name="pref_eq_band_2_title">Equalizer: 250 Hz</string>
|
||||||
|
<string name="pref_eq_band_3_title">Equalizer: 500 Hz</string>
|
||||||
|
<string name="pref_eq_band_4_title">Equalizer: 1 kHz</string>
|
||||||
|
<string name="pref_eq_band_5_title">Equalizer: 2 kHz</string>
|
||||||
|
<string name="pref_eq_band_6_title">Equalizer: 8 kHz</string>
|
||||||
|
<string name="pref_eq_band_7_title">Equalizer: 16 kHz</string>
|
||||||
|
<string name="pref_equalizer_title">Equalizer</string>
|
||||||
|
<string name="pref_equalizer_summary">Pas audio frequenties aan.</string>
|
||||||
|
<string name="pref_equalizer_summary_off">Het aanpassen van audio frequenties is uitgeschakeld.</string>
|
||||||
|
<string name="pref_equalizer_reset_title">Reset Equalizer</string>
|
||||||
|
<string name="pref_preset_selection_title">Selecteer Voorinstelling</string>
|
||||||
|
<string name="pref_preset_selection_summary">Kies een audio voorinstelling.</string>
|
||||||
|
<string name="pref_preset_none">Geen (Handmatig)</string>
|
||||||
|
<string name="pref_preset_rock">Rock</string>
|
||||||
|
<string name="pref_preset_pop">Pop</string>
|
||||||
|
<string name="pref_preset_jazz">Jazz</string>
|
||||||
|
<string name="pref_preset_flat">Plat</string>
|
||||||
|
<string name="loading">Laden…</string>
|
||||||
|
<string name="media_route_menu_title">Cast</string>
|
||||||
|
<string name="pref_visualizer_title">Spectrum Analyser</string>
|
||||||
|
<string name="pref_visualizer_summary">Toon de Spectrum Analyser.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<string name="descr_player_playback_button">Odtwórz/zatrzymaj</string>
|
<string name="descr_player_playback_button">Odtwórz/zatrzymaj</string>
|
||||||
<string name="descr_player_station_image">Ikona stacji</string>
|
<string name="descr_player_station_image">Ikona stacji</string>
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
|
<string name="dialog_add_station_title">Dodaj Stację</string>
|
||||||
<string name="dialog_edit_station_name">Nazwa stacji</string>
|
<string name="dialog_edit_station_name">Nazwa stacji</string>
|
||||||
<string name="dialog_edit_stream_uri">Adres strumienia</string>
|
<string name="dialog_edit_stream_uri">Adres strumienia</string>
|
||||||
<string name="dialog_error_message_default">Wystąpił błąd</string>
|
<string name="dialog_error_message_default">Wystąpił błąd</string>
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<string name="dialog_generic_button_okay">OK</string>
|
<string name="dialog_generic_button_okay">OK</string>
|
||||||
<string name="dialog_generic_details_button">Pokaż szczegóły</string>
|
<string name="dialog_generic_details_button">Pokaż szczegóły</string>
|
||||||
<string name="dialog_opml_import_details_default">Brak dostępnych szczegółów</string>
|
<string name="dialog_opml_import_details_default">Brak dostępnych szczegółów</string>
|
||||||
|
<string name="dialog_restore_collection_replace_existing">Zastąpić bieżącą kolekcję stacji radiowych stacją z kopii zapasowej?</string>
|
||||||
<string name="dialog_yes_no_message_remove_station">Usunąć stację?</string>
|
<string name="dialog_yes_no_message_remove_station">Usunąć stację?</string>
|
||||||
<string name="dialog_yes_no_message_update_station_images">Pobrać najnowszą wersję obrazów wszystkich stacji?</string>
|
<string name="dialog_yes_no_message_update_station_images">Pobrać najnowszą wersję obrazów wszystkich stacji?</string>
|
||||||
<string name="dialog_yes_no_positive_button_default">Tak</string>
|
<string name="dialog_yes_no_positive_button_default">Tak</string>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
<string name="notification_skip_to_previous">Poprzedni</string>
|
<string name="notification_skip_to_previous">Poprzedni</string>
|
||||||
<string name="notification_skip_to_next">Następny</string>
|
<string name="notification_skip_to_next">Następny</string>
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
|
<string name="onboarding_app_description">Zanurz się w dźwięku swojego wyboru!</string>
|
||||||
<string name="onboarding_app_get_started">Zaczynamy</string>
|
<string name="onboarding_app_get_started">Zaczynamy</string>
|
||||||
<!-- Player -->
|
<!-- Player -->
|
||||||
<string name="player_sheet_h2_station_metadata">Obecnie odtwarzane</string>
|
<string name="player_sheet_h2_station_metadata">Obecnie odtwarzane</string>
|
||||||
@@ -64,6 +67,11 @@
|
|||||||
<string name="pref_general_title">Ogólne</string>
|
<string name="pref_general_title">Ogólne</string>
|
||||||
<string name="pref_m3u_export_summary">Zapisz swoje stacje radiowe w pliku listy odtwarzania M3U, który można zaimportować do innych odtwarzaczy.</string>
|
<string name="pref_m3u_export_summary">Zapisz swoje stacje radiowe w pliku listy odtwarzania M3U, który można zaimportować do innych odtwarzaczy.</string>
|
||||||
<string name="pref_m3u_export_title">Eksportuj M3U</string>
|
<string name="pref_m3u_export_title">Eksportuj M3U</string>
|
||||||
|
<string name="pref_pls_export_summary">Zapisz swoje stacje radiowe w pliku listy odtwarzania PLS, który można zaimportować do innych odtwarzaczy.</string>
|
||||||
|
<string name="pref_pls_export_title">Eksportuj PLS</string>
|
||||||
|
<string name="pref_license_title">Ta aplikacja jest open source</string>
|
||||||
|
<string name="pref_license_summary">Licencjonowane na licencji GPLv3</string>
|
||||||
|
<string name="pref_links_title">Linki</string>
|
||||||
<string name="pref_maintenance_title">Zarządzanie</string>
|
<string name="pref_maintenance_title">Zarządzanie</string>
|
||||||
<string name="pref_station_export_summary">Zapisz do pamięci urządzenia całą kolekcję stacji radiowych, w tym obrazy.</string>
|
<string name="pref_station_export_summary">Zapisz do pamięci urządzenia całą kolekcję stacji radiowych, w tym obrazy.</string>
|
||||||
<string name="pref_station_export_title">Eksportuj stacje</string>
|
<string name="pref_station_export_title">Eksportuj stacje</string>
|
||||||
@@ -86,6 +94,10 @@
|
|||||||
<!-- Toasts -->
|
<!-- Toasts -->
|
||||||
<string name="toastmessage_backed_up">kopia zapasowa została wykonana.</string>
|
<string name="toastmessage_backed_up">kopia zapasowa została wykonana.</string>
|
||||||
<string name="toastmessage_copied_to_clipboard">Skopiowano do schowka.</string>
|
<string name="toastmessage_copied_to_clipboard">Skopiowano do schowka.</string>
|
||||||
|
<string name="toastmessage_connection_failed">Nie można nawiązać lub przywrócić połączenia.</string>
|
||||||
|
<string name="toastmessage_preview_playback_started">Rozpoczęto odtwarzanie podglądu.</string>
|
||||||
|
<string name="toastmessage_preview_playback_failed">Nie można odtworzyć podglądu.</string>
|
||||||
|
<string name="toastmessage_save_pls">Zapisywanie stacji radiowych jako PLS…</string>
|
||||||
<string name="toastmessage_error_download_error">Błąd pobierania</string>
|
<string name="toastmessage_error_download_error">Błąd pobierania</string>
|
||||||
<string name="toastmessage_error_restart_playback_failed">Nie można ponownie uruchomić odtwarzania.</string>
|
<string name="toastmessage_error_restart_playback_failed">Nie można ponownie uruchomić odtwarzania.</string>
|
||||||
<string name="toastmessage_install_file_helper">Proszę zainstalować menedżer plików.</string>
|
<string name="toastmessage_install_file_helper">Proszę zainstalować menedżer plików.</string>
|
||||||
@@ -101,4 +113,45 @@
|
|||||||
<!-- Snackbars -->
|
<!-- Snackbars -->
|
||||||
<string name="snackbar_show">Wyświetl</string>
|
<string name="snackbar_show">Wyświetl</string>
|
||||||
<string name="snackbar_update_available">jest dostępna!</string>
|
<string name="snackbar_update_available">jest dostępna!</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Język</string>
|
||||||
|
<string name="pref_language_selection_summary">Aktualny język</string>
|
||||||
|
<string name="pref_language_system">🗺️ System</string>
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_update_collection_title">Aktualizacja Stacji</string>
|
||||||
|
<string name="pref_update_collection_summary">Pobierz najnowszą wersję wszystkich stacji.</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">Pobrać najnowszą wersję wszystkich stacji?</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">Aktualizuj</string>
|
||||||
|
<string name="pref_audio_effects_title">Efekty Dźwiękowe</string>
|
||||||
|
<string name="pref_bass_boost_title">Wzmocnienie Basów</string>
|
||||||
|
<string name="pref_bass_boost_summary">Zwiększ wzmocnienie basów.</string>
|
||||||
|
<string name="pref_reverb_title">Pogłos</string>
|
||||||
|
<string name="pref_reverb_summary">Dostosuj miks pogłosu.</string>
|
||||||
|
<string name="pref_drc_title">Kompresja Zakresu Dynamiki</string>
|
||||||
|
<string name="pref_drc_summary">Kompresuj zakres dynamiki dla spójnej głośności.</string>
|
||||||
|
<string name="pref_eq_low_title">Korektor: 31 Hz</string>
|
||||||
|
<string name="pref_eq_mid_title">Korektor: 125 Hz</string>
|
||||||
|
<string name="pref_eq_high_title">Korektor: 4 kHz</string>
|
||||||
|
<string name="pref_eq_band_1_title">Korektor: 62 Hz</string>
|
||||||
|
<string name="pref_eq_band_2_title">Korektor: 250 Hz</string>
|
||||||
|
<string name="pref_eq_band_3_title">Korektor: 500 Hz</string>
|
||||||
|
<string name="pref_eq_band_4_title">Korektor: 1 kHz</string>
|
||||||
|
<string name="pref_eq_band_5_title">Korektor: 2 kHz</string>
|
||||||
|
<string name="pref_eq_band_6_title">Korektor: 8 kHz</string>
|
||||||
|
<string name="pref_eq_band_7_title">Korektor: 16 kHz</string>
|
||||||
|
<string name="pref_equalizer_title">Korektor</string>
|
||||||
|
<string name="pref_equalizer_summary">Dostosuj częstotliwości audio.</string>
|
||||||
|
<string name="pref_equalizer_summary_off">Dostosowanie częstotliwości audio jest wyłączone.</string>
|
||||||
|
<string name="pref_equalizer_reset_title">Resetuj Korektor</string>
|
||||||
|
<string name="pref_preset_selection_title">Wybierz Preset</string>
|
||||||
|
<string name="pref_preset_selection_summary">Wybierz preset audio.</string>
|
||||||
|
<string name="pref_preset_none">Brak (Ręczny)</string>
|
||||||
|
<string name="pref_preset_rock">Rock</string>
|
||||||
|
<string name="pref_preset_pop">Pop</string>
|
||||||
|
<string name="pref_preset_jazz">Jazz</string>
|
||||||
|
<string name="pref_preset_flat">Płaski</string>
|
||||||
|
<string name="loading">Ładowanie…</string>
|
||||||
|
<string name="media_route_menu_title">Przesyłanie</string>
|
||||||
|
<string name="pref_visualizer_title">Analizator Widma</string>
|
||||||
|
<string name="pref_visualizer_summary">Pokaż Analizator Widma.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<string name="descr_player_playback_button">Играть/пауза</string>
|
<string name="descr_player_playback_button">Играть/пауза</string>
|
||||||
<string name="descr_player_station_image">Изображение станции</string>
|
<string name="descr_player_station_image">Изображение станции</string>
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
|
<string name="dialog_add_station_title">Добавить станцию</string>
|
||||||
<string name="dialog_edit_station_name">Имя станции</string>
|
<string name="dialog_edit_station_name">Имя станции</string>
|
||||||
<string name="dialog_edit_stream_uri">Адрес потока</string>
|
<string name="dialog_edit_stream_uri">Адрес потока</string>
|
||||||
<string name="dialog_error_message_default">Произошла ошибка</string>
|
<string name="dialog_error_message_default">Произошла ошибка</string>
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<string name="dialog_generic_button_okay">ОК</string>
|
<string name="dialog_generic_button_okay">ОК</string>
|
||||||
<string name="dialog_generic_details_button">Показать детали</string>
|
<string name="dialog_generic_details_button">Показать детали</string>
|
||||||
<string name="dialog_opml_import_details_default">Детали недоступны</string>
|
<string name="dialog_opml_import_details_default">Детали недоступны</string>
|
||||||
|
<string name="dialog_restore_collection_replace_existing">Заменить текущую коллекцию радиостанций радиостанцией из резервной копии?</string>
|
||||||
<string name="dialog_yes_no_message_remove_station">Удалить эту станцию?</string>
|
<string name="dialog_yes_no_message_remove_station">Удалить эту станцию?</string>
|
||||||
<string name="dialog_yes_no_message_update_station_images">Скачать последнюю версию всех изображений станций?</string>
|
<string name="dialog_yes_no_message_update_station_images">Скачать последнюю версию всех изображений станций?</string>
|
||||||
<string name="dialog_yes_no_positive_button_default">Да</string>
|
<string name="dialog_yes_no_positive_button_default">Да</string>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
<string name="notification_skip_to_previous">Предыдущий</string>
|
<string name="notification_skip_to_previous">Предыдущий</string>
|
||||||
<string name="notification_skip_to_next">Следующий</string>
|
<string name="notification_skip_to_next">Следующий</string>
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
|
<string name="onboarding_app_description">Погрузитесь в звук по вашему выбору!</string>
|
||||||
<string name="onboarding_app_get_started">Начать</string>
|
<string name="onboarding_app_get_started">Начать</string>
|
||||||
<!-- Player -->
|
<!-- Player -->
|
||||||
<string name="player_sheet_h2_station_metadata">Сейчас играет</string>
|
<string name="player_sheet_h2_station_metadata">Сейчас играет</string>
|
||||||
@@ -91,6 +94,7 @@
|
|||||||
<!-- Toasts -->
|
<!-- Toasts -->
|
||||||
<string name="toastmessage_backed_up">резервная копия создана.</string>
|
<string name="toastmessage_backed_up">резервная копия создана.</string>
|
||||||
<string name="toastmessage_copied_to_clipboard">Скопировано в буфер обмена.</string>
|
<string name="toastmessage_copied_to_clipboard">Скопировано в буфер обмена.</string>
|
||||||
|
<string name="toastmessage_connection_failed">Не удалось установить или восстановить соединение.</string>
|
||||||
<string name="toastmessage_error_download_error">Ошибка загрузки</string>
|
<string name="toastmessage_error_download_error">Ошибка загрузки</string>
|
||||||
<string name="toastmessage_error_restart_playback_failed">Невозможно запустить или перезапустить воспроизведение.</string>
|
<string name="toastmessage_error_restart_playback_failed">Невозможно запустить или перезапустить воспроизведение.</string>
|
||||||
<string name="toastmessage_install_file_helper">Пожалуйста, установите файловый менеджер.</string>
|
<string name="toastmessage_install_file_helper">Пожалуйста, установите файловый менеджер.</string>
|
||||||
@@ -109,4 +113,45 @@
|
|||||||
<!-- Snackbars -->
|
<!-- Snackbars -->
|
||||||
<string name="snackbar_show">Показать</string>
|
<string name="snackbar_show">Показать</string>
|
||||||
<string name="snackbar_update_available">доступно!</string>
|
<string name="snackbar_update_available">доступно!</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Язык</string>
|
||||||
|
<string name="pref_language_selection_summary">Текущий язык</string>
|
||||||
|
<string name="pref_language_system">🗺️ Система</string>
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_update_collection_title">Обновление станций</string>
|
||||||
|
<string name="pref_update_collection_summary">Скачать последнюю версию всех станций.</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">Скачать последнюю версию всех станций?</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">Обновить</string>
|
||||||
|
<string name="pref_audio_effects_title">Звуковые эффекты</string>
|
||||||
|
<string name="pref_bass_boost_title">Усиление басов</string>
|
||||||
|
<string name="pref_bass_boost_summary">Увеличить усиление басов.</string>
|
||||||
|
<string name="pref_reverb_title">Реверберация</string>
|
||||||
|
<string name="pref_reverb_summary">Настроить микс реверберации.</string>
|
||||||
|
<string name="pref_drc_title">Динамическое сжатие</string>
|
||||||
|
<string name="pref_drc_summary">Сжать динамический диапазон для постоянной громкости.</string>
|
||||||
|
<string name="pref_eq_low_title">Эквалайзер: 31 Гц</string>
|
||||||
|
<string name="pref_eq_mid_title">Эквалайзер: 125 Гц</string>
|
||||||
|
<string name="pref_eq_high_title">Эквалайзер: 4 кГц</string>
|
||||||
|
<string name="pref_eq_band_1_title">Эквалайзер: 62 Гц</string>
|
||||||
|
<string name="pref_eq_band_2_title">Эквалайзер: 250 Гц</string>
|
||||||
|
<string name="pref_eq_band_3_title">Эквалайзер: 500 Гц</string>
|
||||||
|
<string name="pref_eq_band_4_title">Эквалайзер: 1 кГц</string>
|
||||||
|
<string name="pref_eq_band_5_title">Эквалайзер: 2 кГц</string>
|
||||||
|
<string name="pref_eq_band_6_title">Эквалайзер: 8 кГц</string>
|
||||||
|
<string name="pref_eq_band_7_title">Эквалайзер: 16 кГц</string>
|
||||||
|
<string name="pref_equalizer_title">Эквалайзер</string>
|
||||||
|
<string name="pref_equalizer_summary">Настроить звуковые частоты.</string>
|
||||||
|
<string name="pref_equalizer_summary_off">Настройка звуковых частот отключена.</string>
|
||||||
|
<string name="pref_equalizer_reset_title">Сбросить эквалайзер</string>
|
||||||
|
<string name="pref_preset_selection_title">Выбрать пресет</string>
|
||||||
|
<string name="pref_preset_selection_summary">Выберите звуковой пресет.</string>
|
||||||
|
<string name="pref_preset_none">Нет (Ручной)</string>
|
||||||
|
<string name="pref_preset_rock">Рок</string>
|
||||||
|
<string name="pref_preset_pop">Поп</string>
|
||||||
|
<string name="pref_preset_jazz">Джаз</string>
|
||||||
|
<string name="pref_preset_flat">Плоский</string>
|
||||||
|
<string name="loading">Загрузка…</string>
|
||||||
|
<string name="media_route_menu_title">Трансляция</string>
|
||||||
|
<string name="pref_visualizer_title">Анализатор спектра</string>
|
||||||
|
<string name="pref_visualizer_summary">Показать анализатор спектра.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<string name="descr_player_playback_button">Відтворити/призупинити</string>
|
<string name="descr_player_playback_button">Відтворити/призупинити</string>
|
||||||
<string name="descr_player_station_image">Зображення станції</string>
|
<string name="descr_player_station_image">Зображення станції</string>
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
|
<string name="dialog_add_station_title">Додати станцію</string>
|
||||||
<string name="dialog_edit_station_name">Назва станції</string>
|
<string name="dialog_edit_station_name">Назва станції</string>
|
||||||
<string name="dialog_edit_stream_uri">Адреса трансляції</string>
|
<string name="dialog_edit_stream_uri">Адреса трансляції</string>
|
||||||
<string name="dialog_error_message_default">Виникла помилка</string>
|
<string name="dialog_error_message_default">Виникла помилка</string>
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<string name="dialog_generic_button_okay">Добре</string>
|
<string name="dialog_generic_button_okay">Добре</string>
|
||||||
<string name="dialog_generic_details_button">Показати подробиці</string>
|
<string name="dialog_generic_details_button">Показати подробиці</string>
|
||||||
<string name="dialog_opml_import_details_default">Подробиці недоступні</string>
|
<string name="dialog_opml_import_details_default">Подробиці недоступні</string>
|
||||||
|
<string name="dialog_restore_collection_replace_existing">Замінити поточну колекцію радіостанцій радіостанцією з резервної копії?</string>
|
||||||
<string name="dialog_yes_no_message_remove_station">Видалити цю станцію?</string>
|
<string name="dialog_yes_no_message_remove_station">Видалити цю станцію?</string>
|
||||||
<string name="dialog_yes_no_message_update_station_images">Завантажити останню версію всіх зображень станцій?</string>
|
<string name="dialog_yes_no_message_update_station_images">Завантажити останню версію всіх зображень станцій?</string>
|
||||||
<string name="dialog_yes_no_positive_button_default">Так</string>
|
<string name="dialog_yes_no_positive_button_default">Так</string>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
<string name="notification_skip_to_previous">Попередня</string>
|
<string name="notification_skip_to_previous">Попередня</string>
|
||||||
<string name="notification_skip_to_next">Наступна</string>
|
<string name="notification_skip_to_next">Наступна</string>
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
|
<string name="onboarding_app_description">Пориньте у звук на ваш вибір!</string>
|
||||||
<string name="onboarding_app_get_started">Початок роботи</string>
|
<string name="onboarding_app_get_started">Початок роботи</string>
|
||||||
<!-- Player -->
|
<!-- Player -->
|
||||||
<string name="player_sheet_h2_station_metadata">Зараз грає</string>
|
<string name="player_sheet_h2_station_metadata">Зараз грає</string>
|
||||||
@@ -110,4 +113,45 @@
|
|||||||
<!-- Snackbars -->
|
<!-- Snackbars -->
|
||||||
<string name="snackbar_show">Показати</string>
|
<string name="snackbar_show">Показати</string>
|
||||||
<string name="snackbar_update_available">доступне!</string>
|
<string name="snackbar_update_available">доступне!</string>
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Мова</string>
|
||||||
|
<string name="pref_language_selection_summary">Поточна мова</string>
|
||||||
|
<string name="pref_language_system">🗺️ Система</string>
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_update_collection_title">Оновлення станцій</string>
|
||||||
|
<string name="pref_update_collection_summary">Завантажити останню версію всіх станцій.</string>
|
||||||
|
<string name="dialog_yes_no_message_update_collection">Завантажити останню версію всіх станцій?</string>
|
||||||
|
<string name="dialog_yes_no_positive_button_update_collection">Оновити</string>
|
||||||
|
<string name="pref_audio_effects_title">Звукові ефекти</string>
|
||||||
|
<string name="pref_bass_boost_title">Підсилення басів</string>
|
||||||
|
<string name="pref_bass_boost_summary">Збільшити підсилення басів.</string>
|
||||||
|
<string name="pref_reverb_title">Реверберація</string>
|
||||||
|
<string name="pref_reverb_summary">Налаштувати мікс реверберації.</string>
|
||||||
|
<string name="pref_drc_title">Динамічна компресія</string>
|
||||||
|
<string name="pref_drc_summary">Стиснути динамічний діапазон для стабільної гучності.</string>
|
||||||
|
<string name="pref_eq_low_title">Еквалайзер: 31 Гц</string>
|
||||||
|
<string name="pref_eq_mid_title">Еквалайзер: 125 Гц</string>
|
||||||
|
<string name="pref_eq_high_title">Еквалайзер: 4 кГц</string>
|
||||||
|
<string name="pref_eq_band_1_title">Еквалайзер: 62 Гц</string>
|
||||||
|
<string name="pref_eq_band_2_title">Еквалайзер: 250 Гц</string>
|
||||||
|
<string name="pref_eq_band_3_title">Еквалайзер: 500 Гц</string>
|
||||||
|
<string name="pref_eq_band_4_title">Еквалайзер: 1 кГц</string>
|
||||||
|
<string name="pref_eq_band_5_title">Еквалайзер: 2 кГц</string>
|
||||||
|
<string name="pref_eq_band_6_title">Еквалайзер: 8 кГц</string>
|
||||||
|
<string name="pref_eq_band_7_title">Еквалайзер: 16 кГц</string>
|
||||||
|
<string name="pref_equalizer_title">Еквалайзер</string>
|
||||||
|
<string name="pref_equalizer_summary">Налаштувати звукові частоти.</string>
|
||||||
|
<string name="pref_equalizer_summary_off">Налаштування звукових частот вимкнено.</string>
|
||||||
|
<string name="pref_equalizer_reset_title">Скинути еквалайзер</string>
|
||||||
|
<string name="pref_preset_selection_title">Вибрати пресет</string>
|
||||||
|
<string name="pref_preset_selection_summary">Виберіть звуковий пресет.</string>
|
||||||
|
<string name="pref_preset_none">Немає (Ручний)</string>
|
||||||
|
<string name="pref_preset_rock">Рок</string>
|
||||||
|
<string name="pref_preset_pop">Поп</string>
|
||||||
|
<string name="pref_preset_jazz">Джаз</string>
|
||||||
|
<string name="pref_preset_flat">Плаский</string>
|
||||||
|
<string name="loading">Завантаження…</string>
|
||||||
|
<string name="media_route_menu_title">Трансляція</string>
|
||||||
|
<string name="pref_visualizer_title">Аналізатор спектру</string>
|
||||||
|
<string name="pref_visualizer_summary">Показати аналізатор спектру.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_banner_background">#2C67E6</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_channel_background">#0A5DBC</color>
|
||||||
|
</resources>
|
||||||
@@ -58,6 +58,21 @@
|
|||||||
<string name="player_sheet_h2_station_metadata">Currently playing</string>
|
<string name="player_sheet_h2_station_metadata">Currently playing</string>
|
||||||
<string name="player_sheet_h2_stream_url">Streaming link</string>
|
<string name="player_sheet_h2_stream_url">Streaming link</string>
|
||||||
|
|
||||||
|
<!-- Language Selection -->
|
||||||
|
<string name="pref_language_selection_title">Language</string>
|
||||||
|
<string name="pref_language_selection_summary">Current language</string>
|
||||||
|
<string name="pref_language_system">🗺️ System</string>
|
||||||
|
<string name="pref_language_en" translatable="false">🇬🇧 English</string>
|
||||||
|
<string name="pref_language_de" translatable="false">🇩🇪 Deutsch</string>
|
||||||
|
<string name="pref_language_fr" translatable="false">🇫🇷 Français</string>
|
||||||
|
<string name="pref_language_ru" translatable="false">🇷🇺 Русский</string>
|
||||||
|
<string name="pref_language_ja" translatable="false">🇯🇵 日本語</string>
|
||||||
|
<string name="pref_language_nl" translatable="false">🇳🇱 Nederlands</string>
|
||||||
|
<string name="pref_language_pl" translatable="false">🇵🇱 Polski</string>
|
||||||
|
<string name="pref_language_el" translatable="false">🇬🇷 Ελληνικά</string>
|
||||||
|
<string name="pref_language_da" translatable="false">🇩🇰 Dansk</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="pref_update_collection_title">Update Stations</string>
|
<string name="pref_update_collection_title">Update Stations</string>
|
||||||
<string name="pref_update_collection_summary">Download latest version of all station.</string>
|
<string name="pref_update_collection_summary">Download latest version of all station.</string>
|
||||||
@@ -171,7 +186,7 @@
|
|||||||
<string name="icon_launcher" translatable="false">Icon launcher.</string>
|
<string name="icon_launcher" translatable="false">Icon launcher.</string>
|
||||||
|
|
||||||
<!-- Extras -->
|
<!-- Extras -->
|
||||||
<string name="loading">Loading...</string>
|
<string name="loading">Loading…</string>
|
||||||
<string name="media_route_menu_title">Cast</string>
|
<string name="media_route_menu_title">Cast</string>
|
||||||
<string name="pref_visualizer_title">Spectrum Analyzer</string>
|
<string name="pref_visualizer_title">Spectrum Analyzer</string>
|
||||||
<string name="pref_visualizer_summary">Show the Spectrum Analyzer.</string>
|
<string name="pref_visualizer_summary">Show the Spectrum Analyzer.</string>
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<locale android:name="en" />
|
<locale android:name="en" />
|
||||||
<locale android:name="ar" />
|
|
||||||
<locale android:name="bg" />
|
|
||||||
<locale android:name="cs" />
|
|
||||||
<locale android:name="de" />
|
<locale android:name="de" />
|
||||||
<locale android:name="el" />
|
<locale android:name="el" />
|
||||||
<locale android:name="fr" />
|
<locale android:name="fr" />
|
||||||
<locale android:name="hu" />
|
|
||||||
<locale android:name="it" />
|
|
||||||
<locale android:name="nl" />
|
<locale android:name="nl" />
|
||||||
<locale android:name="pl" />
|
<locale android:name="pl" />
|
||||||
<locale android:name="pt" />
|
|
||||||
<locale android:name="ru" />
|
<locale android:name="ru" />
|
||||||
<locale android:name="ro" />
|
|
||||||
<locale android:name="tr" />
|
|
||||||
<locale android:name="uk" />
|
<locale android:name="uk" />
|
||||||
<locale android:name="zh-rCN" />
|
|
||||||
<locale android:name="da" />
|
<locale android:name="da" />
|
||||||
<locale android:name="ja" />
|
<locale android:name="ja" />
|
||||||
</locale-config>
|
</locale-config>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
alias libs.plugins.android.application apply false
|
|
||||||
alias libs.plugins.android.library apply false
|
|
||||||
alias libs.plugins.jetbrains.kotlin.android apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('clean', Delete) {
|
|
||||||
delete layout.buildDirectory
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all subprojects/modules.
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.android.library) apply false
|
||||||
|
alias(libs.plugins.jetbrains.kotlin.android) apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(layout.buildDirectory)
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
activityKtx = "1.13.0"
|
activityKtx = "1.13.0"
|
||||||
agp = "9.1.0"
|
agp = "9.1.1"
|
||||||
coreKtx = "1.18.0"
|
coreKtx = "1.18.0"
|
||||||
freedroidwarn = "V1.10"
|
freedroidwarn = "V1.11"
|
||||||
gson = "2.13.2"
|
gson = "2.13.2"
|
||||||
kotlin = "2.3.20"
|
kotlin = "2.3.20"
|
||||||
leanback = "1.2.0"
|
leanback = "1.2.0"
|
||||||
@@ -15,7 +15,7 @@ paletteKtx = "1.0.0"
|
|||||||
preferenceKtx = "1.2.1"
|
preferenceKtx = "1.2.1"
|
||||||
volley = "1.2.1"
|
volley = "1.2.1"
|
||||||
workRuntimeKtx = "2.11.2"
|
workRuntimeKtx = "2.11.2"
|
||||||
playServicesCastFramework = "22.3.0"
|
playServicesCastFramework = "22.3.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
@@ -3,20 +3,22 @@ pluginManagement {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
maven { url 'https://jitpack.io' }
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
@Suppress("UnstableApiUsage")
|
||||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
|
||||||
}
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://jitpack.io' }
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
include ':app'
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
Reference in New Issue
Block a user