38 Commits

Author SHA1 Message Date
Michatec f6208f5e5a style(ui): enable marquee for station metadata and playback controls 2026-04-21 20:04:34 +02:00
Michatec a1beb17b26 style(ui): make language selection dialog scrollable 2026-04-21 19:51:53 +02:00
Michatec 18c28170c5 style(ui): update preference and card icons for presets, equalizer, and visualizer 2026-04-21 19:44:46 +02:00
Michatec 48836334bb refactor(ui): remove unused LanguageSelectionDialog import 2026-04-21 19:26:02 +02:00
Michatec 133c56be4d feat(ui): update and expand translations for multiple languages 2026-04-21 19:15:37 +02:00
Michatec 63d85118a4 feat(ui): add manual language selection to settings 2026-04-21 18:58:53 +02:00
Michatec 4f150221b7 style(ui): add adaptive channel icon 2026-04-20 16:13:28 +02:00
Michatec 5fbf763bd6 style(ui): update banner and banner foreground assets 2026-04-20 14:32:38 +02:00
Michatec 0d3224214e style(ui): update application banner to ic_banner 2026-04-19 12:03:14 +02:00
Michatec 11610084f9 loc(de): add German translations for streaming and loading strings 2026-04-19 11:35:59 +02:00
Michatec 08f303997e refactor(ui): use Locale.ROOT for bitrate formatting 2026-04-19 11:27:54 +02:00
Michatec b60b8cdb7c docs: add SECURITY.md 2026-04-19 00:11:17 +02:00
Michatec b3de68050c refactor(collection): add TODO for notifyDataSetChanged cleanup 2026-04-18 23:35:27 +02:00
Michatec 5d99b2a113 chore: update vscode settings and gradlew reference 2026-04-18 23:29:36 +02:00
Michatec adac340925 style(ui): add tools:visibility to station icon in card layout 2026-04-18 20:23:30 +02:00
Michatec d6037dc0c2 chore(config): prune unsupported locales in locale config 2026-04-18 20:13:04 +02:00
Michatec 297310a784 feat(ui): show snackbar when content is copied to clipboard 2026-04-18 20:08:43 +02:00
Michatec abd9b5ecd9 fix(settings): conditionally enable stream URI editing based on station editing status 2026-04-18 18:39:59 +02:00
Michatec 744a650a91 feat(ui): use checkbox to indicate reordering station status 2026-04-18 17:53:52 +02:00
Michatec 1238a5fba2 style(ui): remove manual card stroke color reset 2026-04-18 16:47:19 +02:00
Michatec cc0b284a7f style(ui): update station card stroke color during reordering and remove play button constraint 2026-04-18 16:31:25 +02:00
Michachatz dfbbd8da33 Fix version formatting in README.md 2026-04-15 21:07:22 +02:00
Michachatz 7690ce5685 Update README to specify Android TV support version 2026-04-15 21:06:42 +02:00
Michachatz 3bbc9280b5 Refactor gradle-publish.yml 2026-04-15 07:18:42 +02:00
Michachatz 0fe32420f2 Merge pull request #61 from Michatec/renovate/agp
chore(deps): update agp to v9.1.1
2026-04-15 06:45:02 +02:00
Michachatz 1491a084f3 Merge pull request #62 from Michatec/renovate/freedroidwarn
fix(deps): update dependency com.github.woheller69:freedroidwarn to v1.11
2026-04-15 06:44:49 +02:00
renovate[bot] 244121edb1 fix(deps): update dependency com.github.woheller69:freedroidwarn to v1.11 2026-04-15 04:39:29 +00:00
renovate[bot] 5479ae25f2 chore(deps): update agp to v9.1.1 2026-04-15 04:39:25 +00:00
Michachatz c0ef50b5a9 Update gradle-publish.yml 2026-04-15 06:38:52 +02:00
Michatec 5fb775a373 ci: update gradle-publish workflow to version artifact with app version name 2026-04-09 16:02:41 +02:00
Michatec 901ae6b8ad build: migrate Gradle build files to Kotlin DSL (KTS) 2026-04-09 15:41:13 +02:00
Michatec 255f27bddf perf(dsp): optimize equalizer and update coefficients calculation 2026-04-08 11:48:56 +02:00
Michatec 45576e1577 refactor(player): remove eighth equalizer band preference 2026-04-08 11:08:41 +02:00
Michatec 1212fc61b5 fix(equalizer): update band mappings and reset all bands 2026-04-08 11:04:12 +02:00
Michatec cdf7668d43 refactor(dsp): improve reverb algorithm and update audio presets 2026-04-07 16:10:50 +02:00
Michatec f755dc5173 perf(dsp): optimize audio processing and atomic memory ordering 2026-04-07 15:55:16 +02:00
Michatec 8c7a8ce7c4 feat(ui): add dedicated station search fragment for television platforms 2026-04-07 12:59:33 +02:00
Michatec d1cc340417 feat(ui): add visualizer button to collection screen 2026-04-07 11:58:23 +02:00
65 changed files with 1452 additions and 248 deletions
+14 -8
View File
@@ -7,10 +7,12 @@ 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 ZIPALIGN: /usr/local/lib/android/sdk/build-tools/34.0.0/zipalign
@@ -30,17 +32,21 @@ 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: 'ndk;29.0.14206865' packages: 'ndk;29.0.14206865'
@@ -67,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
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"C_Cpp.default.compilerPath": "" "C_Cpp.default.compilerPath": "",
"java.configuration.updateBuildConfiguration": "interactive"
} }
+1 -1
View File
@@ -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) and Cast to Devices (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
View File
@@ -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)
-95
View File
@@ -1,95 +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'
}
}
ndkVersion "29.0.14206865"
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
}
+81
View File
@@ -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)
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+84 -60
View File
@@ -19,7 +19,7 @@ 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.1f; 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,
@@ -47,8 +47,6 @@ struct alignas(16) EqBandInterpolator {
inline float process(float x) { inline float process(float x) {
if (!active) return x; if (!active) return x;
updateInterpolation(); updateInterpolation();
float g = currentGain.load(std::memory_order_acquire);
if (std::abs(g) < 0.01f) return x;
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;
@@ -56,10 +54,8 @@ struct alignas(16) EqBandInterpolator {
} }
inline void setCoefficients(float sr, float f, float g, float bw) { inline void setCoefficients(float sr, float f, float g, float bw) {
const bool isActive = std::abs(g) > 0.1f; active = true;
active = isActive; const float A = powf(10.0f, g / 60.0f);
if (!isActive) return;
const float A = powf(10.0f, g / 40.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);
@@ -92,8 +88,6 @@ struct alignas(16) BassFilter {
inline float process(float x) { inline float process(float x) {
if (!active.load(std::memory_order_acquire)) return x; if (!active.load(std::memory_order_acquire)) return x;
updateInterpolation(); updateInterpolation();
float g = currentGain.load(std::memory_order_acquire);
if (std::abs(g) < 0.01f) return x;
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;
@@ -102,7 +96,7 @@ struct alignas(16) BassFilter {
} }
void setCoefficients(float sr, float f, float g, float q){ void setCoefficients(float sr, float f, float g, float q){
float A=powf(10.0f,g/40.0f); float A=powf(10.0f,g/60.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);
@@ -121,48 +115,90 @@ struct alignas(16) BassFilter {
} }
}; };
template<int SIZE>
struct CircularBuffer {
alignas(16) std::array<float, SIZE> data = {};
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;
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:
std::atomic<float> mix{0.0f}; std::atomic<float> mix{0.0f};
inline float process(float x) {
float m = mix.load(std::memory_order_acquire); inline float processSample(float x) {
float m = mix.load(std::memory_order_relaxed);
if (m < 0.01f) return x; 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[static_cast<size_t>(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[static_cast<size_t>(i)].write(x + delayed * combFeedback[static_cast<size_t>(i)] + DENORMAL_OFFSET);
combs[static_cast<size_t>(i)].advance();
} }
out *= 0.25f;
for (int i = 0; i < 2; i++) { return x * (1.0f - m) + (out * 0.125f) * m;
float bufOut = allpasses[static_cast<size_t>(i)].read();
float xOut = -0.5f * out + bufOut;
allpasses[static_cast<size_t>(i)].write(out + 0.5f * bufOut);
allpasses[static_cast<size_t>(i)].advance();
out = xOut;
}
return x * (1.0f - m) + out * m;
} }
inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) { inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) {
float m = mix.load(std::memory_order_acquire); float m = mix.load(std::memory_order_relaxed);
if (m < 0.01f) return; 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;
} }
} }
}; };
@@ -175,16 +211,14 @@ public:
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);
float a = attack.load(std::memory_order_acquire); float r = release.load(std::memory_order_relaxed);
float r = release.load(std::memory_order_acquire); float sr = sampleRate.load(std::memory_order_relaxed);
float sr = sampleRate.load(std::memory_order_acquire);
attackCoef = expf(-1.0f / (a * sr)); attackCoef = expf(-1.0f / (a * sr));
releaseCoef = expf(-1.0f / (r * sr)); releaseCoef = expf(-1.0f / (r * sr));
coefficientsValid = true;
} }
inline void processBlock(float* __restrict__ buffer, int count, float& envelope) { inline void processBlock(float* __restrict__ buffer, int count, float& envelope) {
updateCoefficients(); updateCoefficients();
@@ -204,7 +238,6 @@ public:
} }
}; };
static std::atomic<bool> gEqEnabled{false};
static std::atomic<float> gStereoWidth{1.0f}; static std::atomic<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;
@@ -262,14 +295,6 @@ inline void updateAllEqBands() {
gEqL[b].setCoefficients(sr, EQ_FREQUENCIES[static_cast<size_t>(b)], g, 1.0f); 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); gEqR[b].setCoefficients(sr, EQ_FREQUENCIES[static_cast<size_t>(b)], g, 1.0f);
} }
bool anyActive = false;
for (auto const& band : gEqL) {
if (std::abs(band.targetGain.load(std::memory_order_acquire)) > 0.1f) {
anyActive = true;
break;
}
}
gEqEnabled.store(anyActive, std::memory_order_release);
} }
extern "C" { extern "C" {
@@ -375,19 +400,18 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc
gRightBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768; gRightBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768;
} }
bool eqEnabled = gEqEnabled.load(std::memory_order_acquire);
if (eqEnabled) {
for (int i = 0; i < numFrames; i++) { for (int i = 0; i < numFrames; i++) {
float xL = gLeftBuf[static_cast<size_t>(i)]; float xL = gLeftBuf[static_cast<size_t>(i)];
float xR = gRightBuf[static_cast<size_t>(i)]; float xR = gRightBuf[static_cast<size_t>(i)];
for (int b = 0; b < NUM_EQ_BANDS; b++) { for (int b = 0; b < NUM_EQ_BANDS; b++) {
xL = gEqL[b].process(xL); xL = gEqL[b].process(xL);
xR = gEqR[b].process(xR); xR = gEqR[b].process(xR);
} }
gLeftBuf[static_cast<size_t>(i)] = xL; gLeftBuf[static_cast<size_t>(i)] = xL;
gRightBuf[static_cast<size_t>(i)] = xR; gRightBuf[static_cast<size_t>(i)] = xR;
} }
}
for(int i = 0; i < numFrames; i++) { for(int i = 0; i < numFrames; i++) {
gLeftBuf[static_cast<size_t>(i)] = gBassL.process(gLeftBuf[static_cast<size_t>(i)]); gLeftBuf[static_cast<size_t>(i)] = gBassL.process(gLeftBuf[static_cast<size_t>(i)]);
@@ -396,7 +420,7 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc
gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames); gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames);
float stereoWidth = gStereoWidth.load(std::memory_order_acquire); float stereoWidth = gStereoWidth.load(std::memory_order_relaxed);
if (stereoWidth != 1.0f) { if (stereoWidth != 1.0f) {
float halfWidth = stereoWidth * 0.5f; float halfWidth = stereoWidth * 0.5f;
for (int j = 0; j < numFrames; j++) { for (int j = 0; j < numFrames; j++) {
@@ -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)
}
}
} }
+1 -1
View File
@@ -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
}
}
}
@@ -97,7 +97,7 @@ 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.9f) enableBassBoost(0.9f)
@@ -105,7 +105,7 @@ class NativeAudioProcessor : BaseAudioProcessor() {
fun setPresetPop() { fun setPresetPop() {
enableDrc(true) enableDrc(true)
setReverb(0.10f) 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.6f) enableBassBoost(0.6f)
@@ -263,7 +263,7 @@ object PreferencesHelper {
/* Loads Reverb mix */ /* Loads Reverb mix */
fun loadReverb(): Float { fun loadReverb(): Float {
return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.18f 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"
+12 -1
View File
@@ -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>
+42
View File
@@ -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>
+7
View File
@@ -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>
+45
View File
@@ -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>
+42
View File
@@ -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>
+42
View File
@@ -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>
+55
View File
@@ -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>
+53
View File
@@ -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>
+45
View File
@@ -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>
+44
View File
@@ -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>
+16 -1
View File
@@ -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>
-9
View File
@@ -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>
-11
View File
@@ -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
}
+11
View File
@@ -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)
}
+3 -3
View File
@@ -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" }
Vendored
+1 -1
View File
@@ -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/.
+8 -6
View File
@@ -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")