Skip to content
54 changes: 51 additions & 3 deletions android/app/src/main/kotlin/com/mossapps/flick/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,23 @@ class MainActivity: FlutterActivity() {
)
}

private fun isPhysicalAudioOutput(device: AudioDeviceInfo): Boolean {
return when (device.type) {
AudioDeviceInfo.TYPE_USB_DEVICE,
AudioDeviceInfo.TYPE_USB_HEADSET,
AudioDeviceInfo.TYPE_WIRED_HEADSET,
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
AudioDeviceInfo.TYPE_LINE_ANALOG,
AudioDeviceInfo.TYPE_LINE_DIGITAL,
AudioDeviceInfo.TYPE_AUX_LINE,
AudioDeviceInfo.TYPE_HDMI,
AudioDeviceInfo.TYPE_HDMI_ARC,
AudioDeviceInfo.TYPE_HDMI_EARC,
AudioDeviceInfo.TYPE_DOCK -> true
else -> false
}
}

private fun getPlaybackDeviceInfo(): Map<String, Any?> {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
val outputs = if (audioManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Expand All @@ -4678,18 +4695,47 @@ class MainActivity: FlutterActivity() {
device.type == AudioDeviceInfo.TYPE_USB_DEVICE ||
device.type == AudioDeviceInfo.TYPE_USB_HEADSET
}
val physicalOutputs = outputs.filter { device -> isPhysicalAudioOutput(device) }
val preferredPhysicalOutput = physicalOutputs.minByOrNull { device ->
outputRoutePriority(device.type)
}
val hasWiredAudioRoute = physicalOutputs.any { device ->
routeTypeForAudioDevice(device.type) == "wired"
}
val route = describeCurrentOutputRoute(audioManager)
val usbManager = getSystemService(Context.USB_SERVICE) as? UsbManager
val hasAttachedUac2Device = usbManager
val attachedUac2Device = usbManager
?.deviceList
?.values
?.any { device -> isLikelyUsbAudioDevice(device, route["routeLabel"] as? String) }
?: false
?.firstOrNull { device -> isLikelyUsbAudioDevice(device, route["routeLabel"] as? String) }
val hasAttachedUac2Device = attachedUac2Device != null
val physicalOutputType = preferredPhysicalOutput
?.let { device -> routeTypeForAudioDevice(device.type) }
?: if (hasAttachedUac2Device) "usb" else null
val physicalOutputId = preferredPhysicalOutput?.id?.toString()
?: attachedUac2Device?.deviceName
// Android can expose (or remove) its managed audio route after the USB
// broadcast. Keep the physical connection identity independent of that
// route's AudioDeviceInfo.id, including when libusb owns the device.
val physicalOutputConnectionId = attachedUac2Device?.deviceName
?: preferredPhysicalOutput?.id?.toString()
val physicalOutputLabel = preferredPhysicalOutput?.productName
?.toString()
?.takeIf { it.isNotBlank() }
?: attachedUac2Device?.productName
?.takeIf { it.isNotBlank() }
?: (route["routeLabel"] as? String)

return mapOf(
"hasUsbDac" to (hasUsbAudioRoute || hasAttachedUac2Device),
"hasUsbAudioRoute" to hasUsbAudioRoute,
"hasAttachedUac2Device" to hasAttachedUac2Device,
"hasWiredAudioRoute" to hasWiredAudioRoute,
"hasPhysicalOutput" to (physicalOutputs.isNotEmpty() || hasAttachedUac2Device),
"physicalOutputType" to physicalOutputType,
"physicalOutputId" to physicalOutputId,
"physicalOutputConnectionId" to physicalOutputConnectionId,
"physicalOutputLabel" to physicalOutputLabel,
"manufacturer" to Build.MANUFACTURER,
"model" to Build.MODEL,
"routeType" to route["routeType"],
Expand Down Expand Up @@ -5583,10 +5629,12 @@ class MainActivity: FlutterActivity() {
// (e.g. re-pin from speaker to the 3.5mm jack when headphones
// are plugged in mid-playback).
priorityAnchorService.updateRouteDevice()
audioDeviceChannel?.invokeMethod("onPlaybackDevicesChanged", null)
}

override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
priorityAnchorService.updateRouteDevice()
audioDeviceChannel?.invokeMethod("onPlaybackDevicesChanged", null)
}
}
try {
Expand Down
36 changes: 36 additions & 0 deletions docs/SPANISH_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Spanish translation review

**Draft, not linguistically verified.** The Latin American Spanish translation
is a community contribution from [discussion #368](https://github.com/moss-apps/Flick/discussions/368).
The contributor reports using ChatGPT to assist with translation. Technical
checks and corrections do not replace native-speaker approval.

## Try it

1. Open Settings → Interface → Language and select **Español (Latinoamérica)**.
2. The language option marks the translation as a draft pending native-speaker
review, just as Taglish retains its own draft notice.
3. **English** remains a recognizable escape option in every language.
4. With **System default** selected, Spanish device locales (including `es_MX`
and `es_419`) use this same `es` resource. It is Latin American copy, not a
separate regional translation for each Spanish-speaking country.

The language choice persists across restarts. Taglish's optional Baybayin labels
do not apply to Spanish.

## Review before treating it as approved

- [ ] Native speaker checks natural wording, consistency and Latin American usage.
- [ ] Check onboarding, library navigation, settings, player actions and help on
an Android device, including narrow screens and enlarged text.
- [ ] Check count-dependent messages with zero, one and multiple items, especially
identification, playlist sync and duplicate cleanup.
- [ ] Review audio terminology and warnings: bit-perfect bypass, DSP, DSD/DoP,
fixed USB volume and 432/440 tuning must retain their technical meaning.
- [ ] Confirm library-removal and file-deletion warnings remain unambiguous.
- [ ] Confirm switching back to English and restoring the saved choice work on
a device.

Keep the draft notice until native-speaker review is complete. Automated tests
cover resource parity, arguments, selected wording and language-switch behavior;
they do not approve the translation or replace device testing.
40 changes: 22 additions & 18 deletions lib/features/player/widgets/player_layout_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,15 @@ class _PlayerLayoutSheetState extends ConsumerState<PlayerLayoutSheet> {
color: context.adaptiveTextSecondary,
),
const SizedBox(width: 10),
Text(
l10n.playerLayout,
style: TextStyle(
fontFamily: 'GoogleSansFlex',
fontSize: 18,
fontWeight: FontWeight.w600,
color: context.adaptiveTextPrimary,
Expanded(
child: Text(
l10n.playerLayout,
style: TextStyle(
fontFamily: 'GoogleSansFlex',
fontSize: 18,
fontWeight: FontWeight.w600,
color: context.adaptiveTextPrimary,
),
),
),
],
Expand Down Expand Up @@ -1543,17 +1545,19 @@ class _PlayerActionButtonSelector extends StatelessWidget {
: context.adaptiveTextSecondary,
),
const SizedBox(width: 4),
Text(
action.labelFor(l10n),
style: TextStyle(
fontFamily: 'GoogleSansFlex',
fontSize: 12,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
color: isSelected
? AppColors.textPrimary
: context.adaptiveTextSecondary,
Flexible(
child: Text(
action.labelFor(l10n),
style: TextStyle(
fontFamily: 'GoogleSansFlex',
fontSize: 12,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
color: isSelected
? AppColors.textPrimary
: context.adaptiveTextSecondary,
),
),
),
],
Expand Down
4 changes: 3 additions & 1 deletion lib/features/settings/data/settings_search_catalog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ List<SettingsSearchPage> buildSettingsSearchCatalog() {
'language',
l10n.language,
[],
keywords: 'locale english chinese taglish filipino baybayin',
keywords:
'locale english chinese spanish español espanol latinoamérica latinoamerica taglish filipino baybayin',
),
SettingsSearchSection('interfaceLabel', l10n.interfaceLabel, [
SettingsSearchItem('animations', l10n.animations),
Expand Down Expand Up @@ -583,6 +584,7 @@ List<SettingsSearchPage> buildSettingsSearchCatalog() {
_section('orbitLayoutProfile', l10n.orbitLayoutProfile, [
('portrait', l10n.portrait),
('landscape', l10n.landscape),
('orbitHeaderBoundaryTitle', l10n.orbitHeaderBoundaryTitle),
]),
_section('geometry', l10n.geometry, [
('curvature', l10n.curvature),
Expand Down
12 changes: 12 additions & 0 deletions lib/features/settings/screens/orbit_settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flick/core/constants/app_constants.dart';
import 'package:flick/core/utils/adaptive_layout.dart';
import 'package:flick/providers/app_preferences_provider.dart';
import 'package:flick/features/settings/widgets/orbit_preview.dart';
import 'package:flick/features/settings/widgets/settings_widgets.dart';
import 'package:flick/l10n/settings_l10n.dart';

Expand Down Expand Up @@ -64,6 +65,7 @@ class _OrbitSettingsScreenState extends ConsumerState<OrbitSettingsScreen> {
return SettingsScaffold(
title: l10n.englishForFilipino.customizeOrbital,
initialSection: widget.initialSection,
pinnedHeader: OrbitPreview(landscape: _landscape),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand Down Expand Up @@ -95,6 +97,16 @@ class _OrbitSettingsScreenState extends ConsumerState<OrbitSettingsScreen> {
],
),
const SizedBox(height: AppConstants.spacingLg),
SettingsCard(
children: [
InfoSetting(
icon: LucideIcons.panelTop,
title: l10n.englishForFilipino.orbitHeaderBoundaryTitle,
subtitle: l10n.englishForFilipino.orbitHeaderBoundaryBody,
),
],
),
const SizedBox(height: AppConstants.spacingLg),
SettingsSectionHeader(
l10n.englishForFilipino.geometry,
sectionId: 'geometry',
Expand Down
8 changes: 8 additions & 0 deletions lib/features/settings/widgets/language_settings_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ class LanguageSettingsCard extends StatelessWidget {
onTap: () => LocaleController.instance.select(const Locale('en')),
),
const SettingsDivider(),
SelectionSetting(
icon: LucideIcons.languages,
title: 'Español (Latinoamérica)',
subtitle: l10n.spanishDraftDescription,
selected: selectedLocale?.languageCode == 'es',
onTap: () => LocaleController.instance.select(const Locale('es')),
),
const SettingsDivider(),
SelectionSetting(
icon: LucideIcons.languages,
title: 'Taglish',
Expand Down
Loading
Loading