From d0fdfacd96e1050ed7de622f6ec322369851722a Mon Sep 17 00:00:00 2001 From: moyy Date: Thu, 15 Aug 2024 06:12:58 +0000 Subject: [PATCH 01/14] Update Simplified/Traditional Chinese strings.xml. --- app/src/main/res/values-zh-rCN/strings.xml | 112 +++++++++++++++------ app/src/main/res/values-zh-rTW/strings.xml | 64 ++++++++++-- 2 files changed, 141 insertions(+), 35 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9575c5835..122165c3e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -5,6 +5,7 @@ 音乐人 专辑 播放列表 + 探索 @@ -14,12 +15,16 @@ 历史记录 统计 - Mood and Genres - Account + 氛围和流派 + 账号 歌曲快选 - Listen to songs to generate your quick picks + 播放几首歌为你生成推荐 新专辑 - + 被遗忘的最爱 + 类似于: + 继续聆听 + 你的 YouTube 播放列表 + 今天 昨天 @@ -27,17 +32,17 @@ 上周 - 最常播放 - Most played artists - Most played albums + 最常播放的歌曲 + 最常播放的艺术家 + 最常播放的专辑 搜索 搜索 YouTube Music… 搜索媒体库… - Library - Liked - Downloaded + 媒体库 + 喜欢 + 已下载歌曲 全部 歌曲 视频 @@ -46,22 +51,33 @@ 播放列表 社区播放列表 精选播放列表 - Bookmarked + 书签 找不到结果 来自您的媒体库 + + 其他版本 + + + 喜欢 + 下载 + 我的 Top + + 喜欢的歌曲 已下载的歌曲 播放列表为空 + 确定要删除「%s」的下载吗? + 確定要刪除播放清單「%s」吗? 重试 电台 随机播放 - Reset + 重置 详情 @@ -73,7 +89,7 @@ 加入媒体库 从媒体库中移除 下载 - Downloading + 下载中 删除下载 导入播放列表 加入播放列表 @@ -85,17 +101,20 @@ 从记录中移除 在线搜索 同步 - Advanced - + 高级设置 + 选择全部 + 喜欢全部 + 新建时间 + 更新时间 名称 音乐人 年份 歌曲总数 长度 播放时间 - Custom order + 自定义排序 媒体 ID @@ -124,6 +143,7 @@ 新建播放列表 名称 播放列表名称不能为空 + 已添加到播放列表: 编辑音乐人 音乐人名称 @@ -131,38 +151,50 @@ + %d 首歌曲 %d 首歌曲 + %d 位音乐人 %d 位音乐人 + %d 张专辑 %d 张专辑 + %d 个播放列表 %d 个播放列表 - %d weeks + %d 周 + %d 周 - %d months + %d 月 + %d 月 - %d years + %d 年 + %d 年 + + + %d element + %d elements 已导入此播放列表 - Removed \"%s\" from playlist - Playlist synced - Undo + 从播放列表中删除「%s」 + 播放列表同步完成 + 撤销 没有歌词 睡眠定时器 这首歌曲播放完毕 + 1 分钟 %d 分钟 没有可用音源 @@ -173,11 +205,11 @@ 喜欢 取消喜欢 - Shuffle on - Shuffle off - Repeat mode off - Repeat current song - Repeat queue + 开启随机播放 + 关闭随机播放 + 重复模式关闭 + 重复当前歌曲 + 重复当前列表 全部歌曲 @@ -194,6 +226,11 @@ 跟随系统 + 播放背景风格 + 跟随主题 + 渐变 + 启用波浪滑块 + 启用轻扫缩略图更换歌曲 纯黑 默认启动选项卡 自定义导航选项卡 @@ -201,15 +238,23 @@ 靠左 置中 靠右 - + 点击修改歌词 + 网格单元大小 + + + 内容 登录 默认内容语言 默认内容国家 系统默认 + 代理 启用代理 代理类型 代理 URL + 更改默认媒体库首页 + 设置快速选择 + 根据上次播放的歌曲 重启以应用变更 播放器与音频 @@ -243,11 +288,13 @@ 清除搜索记录 您确定要清除所有搜索记录吗? 使用酷狗音乐提供歌词 + 使用 LrcLib 提供歌词 + 设置首选的歌词提供服务商 备份与还原 备份 还原 - Imported playlist + 导入播放列表 成功新建备份 无法新建备份 无法还原备份 @@ -258,4 +305,11 @@ New version available Translation Models Clear translation models + + 全部时间 + 过去 24 小时 + 过去 1 周 + 过去 1 月 + 过去 1 年 + My Top 的长度 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 66e803e40..715f76f6e 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,10 +1,11 @@ - + 首頁 歌曲 藝人 專輯 播放清單 + 探索 @@ -19,6 +20,10 @@ 歌曲快選 聽一些音樂讓我們知道您的喜好 新專輯 + 被遺忘的最愛 + 類似於: + 繼續聆聽 + 你的 YouTube 播放清單 今天 @@ -52,6 +57,15 @@ 來自你的媒體庫 + + 其他版本 + + + 喜歡 + 下載 + 我的 Top + + 喜歡的歌曲 已下載的歌曲 @@ -88,9 +102,12 @@ 線上搜尋 同步 進階 + 選擇全部 + 喜歡全部 新增時間 + 更新時間 名稱 藝人 年份 @@ -126,6 +143,7 @@ 新增播放清單 名稱 播放清單名稱不能為空 + 已添加到播放清單: 編輯藝人 藝人名稱 @@ -133,26 +151,37 @@ + %d 首歌曲 %d 首歌曲 + %d 位藝人 %d 位藝人 + %d 張專輯 %d 張專輯 + %d 個播放清單 %d 個播放清單 - + + %d 週 %d 週 - %d 個月 + %d 月 + %d 月 + %d 年 %d 年 + + %d element + %d elements + 已匯入此播放清單 @@ -165,6 +194,7 @@ 睡眠定時器 這首歌曲播放完畢 + 1 分鐘 %d 分鐘 沒有可用的音源 @@ -196,6 +226,11 @@ 跟隨系統 + 播放背景風格 + 跟隨主題 + 漸層 + 啟用波浪滑塊 + 啟用輕掃縮圖更換歌曲 純黑 預設啟動標籤 自訂導覽列 @@ -203,15 +238,23 @@ 靠左 置中 靠右 + 點擊修改歌詞 + 網格單元大小 + + 內容 登入 預設內容語言 預設內容國家 系統預設 - 啟用 Proxy - Proxy 種類 - Proxy URL + 代理 + 啟用代理 + 代理類型 + 代理 URL + 更改預設媒體庫首頁 + 設定快速選擇 + 根據上次播放的歌曲 重啟以套用變更 播放與音訊 @@ -245,6 +288,8 @@ 清除搜尋記錄 您確定要清除所有搜尋記錄嗎? 使用酷狗音樂提供歌詞 + 使用 LrcLib 提供歌詞 + 設定首選的歌詞提供服務商 備份與還原 備份 @@ -260,4 +305,11 @@ 發現新版本 翻譯模型 清除翻譯模型 + + 全部時間 + 過去 24 小時 + 過去 1 周 + 過去 1 月 + 過去 1 年 + 我的最愛長度 From b69217b93b9b1cc331ec6796bb21faec46608509 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 22 Aug 2024 09:20:15 +0800 Subject: [PATCH 02/14] feat: predictive back --- app/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f1260c668..754437ff6 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,7 +27,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.InnerTune" - tools:targetApi="o"> + android:enableOnBackInvokedCallback="true" + tools:targetApi="tiramisu"> From c29ab7d8a3811df5d0170c65bfaab36f5f614f92 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 22 Aug 2024 09:32:16 +0800 Subject: [PATCH 03/14] fix(local search): chip color --- .../java/com/malopieds/innertune/ui/component/ChipsRow.kt | 4 +++- .../innertune/ui/screens/search/LocalSearchScreen.kt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt b/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt index 5b283cce7..2ae00a1f5 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @Composable @@ -20,6 +21,7 @@ fun ChipsRow( currentValue: E, onValueUpdate: (E) -> Unit, modifier: Modifier = Modifier, + containerColor: Color = MaterialTheme.colorScheme.surface, ) { Row( modifier = @@ -33,7 +35,7 @@ fun ChipsRow( FilterChip( label = { Text(label) }, selected = currentValue == value, - colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.surface), + colors = FilterChipDefaults.filterChipColors(containerColor = containerColor), onClick = { onValueUpdate(value) }, ) diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/search/LocalSearchScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/search/LocalSearchScreen.kt index e99e68afc..6ec12d64e 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/search/LocalSearchScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/search/LocalSearchScreen.kt @@ -97,6 +97,7 @@ fun LocalSearchScreen( ), currentValue = searchFilter, onValueUpdate = { viewModel.filter.value = it }, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, ) LazyColumn( From e420c8a9c25da9350c94d82ca7675fe6a10738cd Mon Sep 17 00:00:00 2001 From: Malopieds Date: Fri, 23 Aug 2024 08:56:29 +0200 Subject: [PATCH 04/14] feat: language selector, closes #228 credits DanielSevillano --- .../ui/screens/settings/ContentSettings.kt | 26 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 +++ 2 files changed, 29 insertions(+) diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt index 5379bae82..8294853eb 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt @@ -1,5 +1,11 @@ package com.malopieds.innertune.ui.screens.settings +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -17,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.navigation.NavController @@ -60,6 +67,7 @@ fun ContentSettings( navController: NavController, scrollBehavior: TopAppBarScrollBehavior, ) { + val context = LocalContext.current val accountName by rememberPreference(AccountNameKey, "") val accountEmail by rememberPreference(AccountEmailKey, "") val accountChannelHandle by rememberPreference(AccountChannelHandleKey, "") @@ -98,6 +106,7 @@ fun ContentSettings( icon = { Icon(painterResource(R.drawable.person), null) }, onClick = { navController.navigate("login") }, ) + ListPreference( title = { Text(stringResource(R.string.content_language)) }, icon = { Icon(painterResource(R.drawable.language), null) }, @@ -123,6 +132,23 @@ fun ContentSettings( onValueSelected = onContentCountryChange, ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + PreferenceEntry( + title = { Text(stringResource(R.string.app_language)) }, + description = stringResource(R.string.configure_app_language), + icon = { Icon(painterResource(R.drawable.language), null) }, + onClick = { + try { + context.startActivity( + Intent(Settings.ACTION_APPLICATION_SETTINGS, Uri.parse("package:${context.packageName}")), + ) + } catch (e: ActivityNotFoundException) { + Toast.makeText(context, R.string.intent_app_language_not_found, Toast.LENGTH_LONG).show() + } + }, + ) + } + SwitchPreference( title = { Text(stringResource(R.string.hide_explicit)) }, icon = { Icon(painterResource(R.drawable.explicit), null) }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58ac575a6..3deff27fb 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -267,6 +267,9 @@ Set Quick Picks Based on last song listened Restart to take effect + App language + Configure app languages + Couldn\'t find app language settings, please configure them manually Player and audio Audio quality From cfe1e26a1bfe230af17a4cbcb87f28807581d34a Mon Sep 17 00:00:00 2001 From: Malopieds Date: Fri, 23 Aug 2024 09:02:35 +0200 Subject: [PATCH 05/14] feat: add supported links by default, closes #72 credits DanielSevillano --- .../ui/screens/settings/ContentSettings.kt | 17 +++++++++++++++++ app/src/main/res/drawable/add_link.xml | 5 +++++ app/src/main/res/values/strings.xml | 3 +++ 3 files changed, 25 insertions(+) create mode 100644 app/src/main/res/drawable/add_link.xml diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt index 8294853eb..22f79fae9 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/settings/ContentSettings.kt @@ -149,6 +149,23 @@ fun ContentSettings( ) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PreferenceEntry( + title = { Text(stringResource(R.string.open_supported_links)) }, + description = stringResource(R.string.configure_supported_links), + icon = { Icon(painterResource(R.drawable.add_link), null) }, + onClick = { + try { + context.startActivity( + Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, Uri.parse("package:${context.packageName}")), + ) + } catch (e: ActivityNotFoundException) { + Toast.makeText(context, R.string.intent_supported_links_not_found, Toast.LENGTH_LONG).show() + } + }, + ) + } + SwitchPreference( title = { Text(stringResource(R.string.hide_explicit)) }, icon = { Icon(painterResource(R.drawable.explicit), null) }, diff --git a/app/src/main/res/drawable/add_link.xml b/app/src/main/res/drawable/add_link.xml new file mode 100644 index 000000000..eab366311 --- /dev/null +++ b/app/src/main/res/drawable/add_link.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3deff27fb..208d30d5d 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -270,6 +270,9 @@ App language Configure app languages Couldn\'t find app language settings, please configure them manually + Open supported link by default + Configure supported links + Couldn\'t find supported links settings, please configure them manually Player and audio Audio quality From b489633144381aa0f870fbcf1b896bd6033defa1 Mon Sep 17 00:00:00 2001 From: Malopieds Date: Fri, 23 Aug 2024 09:42:11 +0200 Subject: [PATCH 06/14] fix: app crash after deleting songs, try fix for #335 --- .../com/malopieds/innertune/ui/menu/SelectionSongsMenu.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/malopieds/innertune/ui/menu/SelectionSongsMenu.kt b/app/src/main/java/com/malopieds/innertune/ui/menu/SelectionSongsMenu.kt index 60fe97d9e..23561b747 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/menu/SelectionSongsMenu.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/menu/SelectionSongsMenu.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.net.toUri +import androidx.media3.common.Player import androidx.media3.common.Timeline import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.DownloadRequest @@ -540,8 +541,9 @@ fun SelectionMediaMetadataMenu( onDismiss() var i = 0 currentItems.forEach { cur -> - playerConnection.player.removeMediaItem(cur.firstPeriodIndex - i) - i++ + if (playerConnection.player.availableCommands.contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { + playerConnection.player.removeMediaItem(cur.firstPeriodIndex - i++) + } } clearAction() } From f9fb211498693ceaabfd58c938b080c08650b35b Mon Sep 17 00:00:00 2001 From: Malopieds Date: Fri, 23 Aug 2024 14:02:20 +0200 Subject: [PATCH 07/14] feat: update libs, closes #333 --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39ea78178..d0cd7e41a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ json = "20240303" kotlin = "2.0.0" compose = "1.6.8" lifecycle = "2.8.4" -material3 = "1.3.0-beta05" +material3 = "1.3.0-rc01" media3 = "1.4.0" room = "2.6.1" hilt = "2.51.1" @@ -21,7 +21,7 @@ concurrent-futures = { group = "androidx.concurrent", name = "concurrent-futures gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } activity = { group = "androidx.activity", name = "activity-compose", version = "1.9.1" } -navigation = { group = "androidx.navigation", name = "navigation-compose", version = "2.7.7" } +navigation = { group = "androidx.navigation", name = "navigation-compose", version = "2.8.0-rc01" } hilt-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.2.0" } datastore = { group = "androidx.datastore", name = "datastore-preferences", version = "1.1.1" } From 642b17b5f66700639bd63efa47592bd58406664f Mon Sep 17 00:00:00 2001 From: Malopieds Date: Fri, 23 Aug 2024 18:01:43 +0200 Subject: [PATCH 08/14] feat: new stats screen, #21 --- .../innertune/constants/StatPeriod.kt | 38 +++ .../com/malopieds/innertune/db/DatabaseDao.kt | 96 +++++- .../malopieds/innertune/db/entities/Album.kt | 2 + .../malopieds/innertune/db/entities/Artist.kt | 1 + .../innertune/db/entities/SongWithStats.kt | 12 + .../innertune/ui/component/ChipsRow.kt | 136 ++++++++ .../malopieds/innertune/ui/component/Items.kt | 59 ++++ .../innertune/ui/screens/AlbumScreen.kt | 2 +- .../innertune/ui/screens/StatsScreen.kt | 306 ++++++++++++------ .../ui/screens/library/LibrarySongsScreen.kt | 2 +- .../ui/screens/playlist/TopPlaylistScreen.kt | 2 +- .../innertune/viewmodels/StatsViewModel.kt | 107 +++++- app/src/main/res/values-DE/strings.xml | 2 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-fr-rFR/strings.xml | 2 +- app/src/main/res/values-ja-rJP/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values/strings.xml | 10 +- 20 files changed, 652 insertions(+), 135 deletions(-) create mode 100644 app/src/main/java/com/malopieds/innertune/db/entities/SongWithStats.kt diff --git a/app/src/main/java/com/malopieds/innertune/constants/StatPeriod.kt b/app/src/main/java/com/malopieds/innertune/constants/StatPeriod.kt index d016b8e32..8bd4a81b5 100644 --- a/app/src/main/java/com/malopieds/innertune/constants/StatPeriod.kt +++ b/app/src/main/java/com/malopieds/innertune/constants/StatPeriod.kt @@ -1,5 +1,6 @@ package com.malopieds.innertune.constants +import com.malopieds.innertune.ui.screens.OptionStats import java.time.LocalDateTime import java.time.ZoneOffset @@ -47,3 +48,40 @@ enum class StatPeriod { ALL -> 0 } } + +fun statToPeriod( + selection: OptionStats, + test: Int, +): Long = + when (selection) { + OptionStats.WEEKS -> { + LocalDateTime + .now() + .minusWeeks(test.toLong()) + .minusDays(1) + .toInstant(ZoneOffset.UTC) + .toEpochMilli() + } + OptionStats.MONTHS -> { + LocalDateTime + .now() + .withDayOfMonth(1) + .minusMonths(test.toLong()) + .toInstant(ZoneOffset.UTC) + .toEpochMilli() + } + OptionStats.YEARS -> { + LocalDateTime + .now() + .withDayOfMonth(1) + .withMonth(1) + .minusYears(test.toLong()) + .toInstant( + ZoneOffset.UTC, + ).toEpochMilli() + } + OptionStats.CONTINUOUS -> { + val index = if (test > StatPeriod.entries.size) 0 else test + StatPeriod.entries[index].toTimeMillis() + } + } diff --git a/app/src/main/java/com/malopieds/innertune/db/DatabaseDao.kt b/app/src/main/java/com/malopieds/innertune/db/DatabaseDao.kt index 706ac5838..f9731a8fd 100644 --- a/app/src/main/java/com/malopieds/innertune/db/DatabaseDao.kt +++ b/app/src/main/java/com/malopieds/innertune/db/DatabaseDao.kt @@ -38,6 +38,7 @@ import com.malopieds.innertune.db.entities.Song import com.malopieds.innertune.db.entities.SongAlbumMap import com.malopieds.innertune.db.entities.SongArtistMap import com.malopieds.innertune.db.entities.SongEntity +import com.malopieds.innertune.db.entities.SongWithStats import com.malopieds.innertune.extensions.reversed import com.malopieds.innertune.extensions.toSQLiteQuery import com.malopieds.innertune.models.MediaMetadata @@ -47,6 +48,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import java.text.Collator import java.time.LocalDateTime +import java.time.ZoneOffset import java.util.Locale @Dao @@ -311,11 +313,52 @@ interface DatabaseDao { @Transaction @Query( """ - SELECT song.* + SELECT song.id, song.title, song.thumbnailUrl, + (SELECT COUNT(1) + FROM event + WHERE songId = song.id + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS songCountListened, + (SELECT SUM(event.playTime) + FROM event + WHERE songId = song.id + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS timeListened FROM song JOIN (SELECT songId FROM event WHERE timestamp > :fromTimeStamp + AND timestamp <= :toTimeStamp + GROUP BY songId + ORDER BY SUM(playTime) DESC + LIMIT :limit) + ON song.id = songId + LIMIT :limit + OFFSET :offset + """, + ) + fun mostPlayedSongsStats( + limit: Int = 6, + offset: Int = 0, + fromTimeStamp: Long, + toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(), + ): Flow> + + @Transaction + @Query( + """ + SELECT song.*, + (SELECT COUNT(1) + FROM event + WHERE songId = song.id + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS songCountListened, + (SELECT SUM(event.playTime) + FROM event + WHERE songId = song.id + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS timeListened + FROM song + JOIN (SELECT songId + FROM event + WHERE timestamp > :fromTimeStamp + AND timestamp <= :toTimeStamp GROUP BY songId ORDER BY SUM(playTime) DESC LIMIT :limit) @@ -328,6 +371,7 @@ interface DatabaseDao { fromTimeStamp: Long, limit: Int = 6, offset: Int = 0, + toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(), ): Flow> @Transaction @@ -338,13 +382,19 @@ interface DatabaseDao { FROM song_artist_map JOIN event ON song_artist_map.songId = event.songId WHERE artistId = artist.id - AND timestamp > :fromTimeStamp) AS songCount + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS songCount, + (SELECT SUM(event.playTime) + FROM song_artist_map + JOIN event ON song_artist_map.songId = event.songId + WHERE artistId = artist.id + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS timeListened FROM artist JOIN(SELECT artistId, SUM(songTotalPlayTime) AS totalPlayTime FROM song_artist_map JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime FROM event WHERE timestamp > :fromTimeStamp + AND timestamp <= :toTimeStamp GROUP BY songId) AS e ON song_artist_map.songId = e.songId GROUP BY artistId @@ -358,30 +408,42 @@ interface DatabaseDao { fromTimeStamp: Long, limit: Int = 6, offset: Int = 0, + toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(), ): Flow> @Transaction @Query( """ - SELECT album.* + SELECT album.*, + (SELECT COUNT(1) + FROM song_album_map + JOIN event ON song_album_map.songId = event.songId + WHERE albumId = album.id + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS songCountListened, + (SELECT SUM(event.playTime) + FROM song_album_map + JOIN event ON song_album_map.songId = event.songId + WHERE albumId = album.id + AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS timeListened FROM album - JOIN(SELECT albumId - FROM song - JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime - FROM event - WHERE timestamp > :fromTimeStamp - GROUP BY songId) AS e - ON song.id = e.songId - WHERE albumId IS NOT NULL - GROUP BY albumId - ORDER BY SUM(songTotalPlayTime) DESC - LIMIT :limit) - ON album.id = albumId + WHERE id IN (SELECT song.albumId + FROM event + JOIN + song + ON event.songId = song.id + WHERE event.timestamp > :fromTimeStamp + AND event.timestamp <= :toTimeStamp + GROUP BY song.albumId + HAVING song.albumId IS NOT NULL) + ORDER BY timeListened DESC + LIMIT :limit OFFSET :offset """, ) fun mostPlayedAlbums( fromTimeStamp: Long, limit: Int = 6, + offset: Int = 0, + toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(), ): Flow> @Transaction @@ -773,6 +835,10 @@ interface DatabaseDao { @Query("SELECT * FROM event ORDER BY rowId DESC") fun events(): Flow> + @Transaction + @Query("SELECT * FROM event ORDER BY rowId ASC LIMIT 1") + fun firstEvent(): Flow + @Query("DELETE FROM event") fun clearListenHistory() diff --git a/app/src/main/java/com/malopieds/innertune/db/entities/Album.kt b/app/src/main/java/com/malopieds/innertune/db/entities/Album.kt index d1aedd53c..aec3a7420 100644 --- a/app/src/main/java/com/malopieds/innertune/db/entities/Album.kt +++ b/app/src/main/java/com/malopieds/innertune/db/entities/Album.kt @@ -21,6 +21,8 @@ data class Album( ), ) val artists: List, + val songCountListened: Int? = 0, + val timeListened: Int? = 0, ) : LocalItem() { override val id: String get() = album.id diff --git a/app/src/main/java/com/malopieds/innertune/db/entities/Artist.kt b/app/src/main/java/com/malopieds/innertune/db/entities/Artist.kt index a41a91f46..ff2f40bc6 100644 --- a/app/src/main/java/com/malopieds/innertune/db/entities/Artist.kt +++ b/app/src/main/java/com/malopieds/innertune/db/entities/Artist.kt @@ -8,6 +8,7 @@ data class Artist( @Embedded val artist: ArtistEntity, val songCount: Int, + val timeListened: Int? = 0, ) : LocalItem() { override val id: String get() = artist.id diff --git a/app/src/main/java/com/malopieds/innertune/db/entities/SongWithStats.kt b/app/src/main/java/com/malopieds/innertune/db/entities/SongWithStats.kt new file mode 100644 index 000000000..cc8022c06 --- /dev/null +++ b/app/src/main/java/com/malopieds/innertune/db/entities/SongWithStats.kt @@ -0,0 +1,12 @@ +package com.malopieds.innertune.db.entities + +import androidx.compose.runtime.Immutable + +@Immutable +data class SongWithStats( + val id: String, + val title: String, + val thumbnailUrl: String, + val songCountListened: Int, + val timeListened: Long?, +) diff --git a/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt b/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt index 2ae00a1f5..8a713fc52 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/component/ChipsRow.kt @@ -1,19 +1,47 @@ package com.malopieds.innertune.ui.component +import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandIn +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.AssistChip +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.malopieds.innertune.R +import com.malopieds.innertune.ui.screens.OptionStats @Composable fun ChipsRow( @@ -43,3 +71,111 @@ fun ChipsRow( } } } + +@SuppressLint("UnusedContentLambdaTargetStateParameter") +@Composable +fun ChoiceChipsRow( + chips: List>, + options: List>, + selectedOption: OptionStats, + onSelectionChange: (OptionStats) -> Unit, + currentValue: Int, + onValueUpdate: (Int) -> Unit, + modifier: Modifier = Modifier, + containerColor: Color = MaterialTheme.colorScheme.surface, +) { + var expandIconDegree by remember { mutableFloatStateOf(0f) } + val rotationAnimation by animateFloatAsState( + targetValue = expandIconDegree, + animationSpec = tween(durationMillis = 400), + label = "", + ) + + Row( + modifier = + modifier + .fillMaxWidth() + .padding(start = 12.dp), + ) { + var expanded by remember { mutableStateOf(false) } + + Column { + AssistChip( + onClick = { + expanded = !expanded + expandIconDegree -= 180 + }, + label = { + Text( + text = + when (selectedOption) { + OptionStats.WEEKS -> stringResource(id = R.string.weeks) + OptionStats.MONTHS -> stringResource(id = R.string.months) + OptionStats.YEARS -> stringResource(id = R.string.years) + OptionStats.CONTINUOUS -> stringResource(id = R.string.continuous) + }, + ) + }, + trailingIcon = { + Icon( + painter = painterResource(R.drawable.expand_more), + contentDescription = null, + modifier = Modifier.graphicsLayer(rotationZ = rotationAnimation), + ) + }, + ) + + AnimatedVisibility( + visible = expanded, + enter = expandIn() + fadeIn(), + exit = shrinkOut() + fadeOut(), + ) { + DropdownMenu( + modifier = + Modifier + .padding(start = 12.dp), + expanded = expanded, + onDismissRequest = { + expanded = false + expandIconDegree -= 180 + }, + ) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(text = option.second) }, + onClick = { + onSelectionChange(option.first) + expandIconDegree -= 180 + expanded = false + }, + ) + } + } + } + } + + AnimatedContent( + targetState = selectedOption, + transitionSpec = { slideInHorizontally() + fadeIn() togetherWith slideOutHorizontally() + fadeOut() }, + label = "", + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + ) { + chips.forEach { (value, label) -> + Spacer(Modifier.width(8.dp)) + + FilterChip( + label = { Text(label) }, + selected = currentValue == value, + colors = FilterChipDefaults.filterChipColors(containerColor = containerColor), + onClick = { onValueUpdate(value) }, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/malopieds/innertune/ui/component/Items.kt b/app/src/main/java/com/malopieds/innertune/ui/component/Items.kt index 636714aec..d35d80442 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/component/Items.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/component/Items.kt @@ -1670,3 +1670,62 @@ fun YouTubeSmallGridItem( else -> false }, ) + +@Composable +fun LocalItemsGrid( + title: String, + subtitle: String, + badges: + @Composable() + (RowScope.() -> Unit) = {}, + thumbnailUrl: String?, + isActive: Boolean = false, + isPlaying: Boolean = false, + fillMaxWidth: Boolean = false, + modifier: Modifier, +) = GridItem( + title = title, + subtitle = subtitle, + badges = badges, + thumbnailContent = { + AsyncImage( + model = thumbnailUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + + AnimatedVisibility( + visible = isActive, + enter = fadeIn(tween(500)), + exit = fadeOut(tween(500)), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier + .fillMaxSize() + .background( + color = Color.Black.copy(alpha = 0.4f), + shape = RoundedCornerShape(ThumbnailCornerRadius), + ), + ) { + if (isPlaying) { + PlayingIndicator( + color = Color.White, + modifier = Modifier.height(24.dp), + ) + } else { + Icon( + painter = painterResource(R.drawable.play), + contentDescription = null, + tint = Color.White, + ) + } + } + } + }, + thumbnailShape = RoundedCornerShape(ThumbnailCornerRadius), + fillMaxWidth = fillMaxWidth, + modifier = modifier, +) diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/AlbumScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/AlbumScreen.kt index 40b8ccb45..5d17cca7a 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/AlbumScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/AlbumScreen.kt @@ -391,7 +391,7 @@ fun AlbumScreen( ) { if (selection) { val count = wrappedSongs?.count { it.isSelected } ?: 0 - Text(text = pluralStringResource(R.plurals.n_elements, count, count), modifier = Modifier.weight(1f)) + Text(text = pluralStringResource(R.plurals.n_element, count, count), modifier = Modifier.weight(1f)) IconButton( onClick = { if (count == wrappedSongs?.size) { diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/StatsScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/StatsScreen.kt index b1144d6aa..4029d37db 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/StatsScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/StatsScreen.kt @@ -9,10 +9,9 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -35,18 +34,20 @@ import com.malopieds.innertune.constants.StatPeriod import com.malopieds.innertune.extensions.togglePlayPause import com.malopieds.innertune.models.toMediaMetadata import com.malopieds.innertune.playback.queues.YouTubeQueue -import com.malopieds.innertune.ui.component.AlbumGridItem -import com.malopieds.innertune.ui.component.ArtistGridItem -import com.malopieds.innertune.ui.component.ChipsRow +import com.malopieds.innertune.ui.component.ChoiceChipsRow import com.malopieds.innertune.ui.component.IconButton +import com.malopieds.innertune.ui.component.LocalItemsGrid import com.malopieds.innertune.ui.component.LocalMenuState import com.malopieds.innertune.ui.component.NavigationTitle -import com.malopieds.innertune.ui.component.SongListItem import com.malopieds.innertune.ui.menu.AlbumMenu import com.malopieds.innertune.ui.menu.ArtistMenu import com.malopieds.innertune.ui.menu.SongMenu import com.malopieds.innertune.ui.utils.backToMain +import com.malopieds.innertune.utils.joinByBullet +import com.malopieds.innertune.utils.makeTimeString import com.malopieds.innertune.viewmodels.StatsViewModel +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable @@ -60,13 +61,93 @@ fun StatsScreen( val isPlaying by playerConnection.isPlaying.collectAsState() val mediaMetadata by playerConnection.mediaMetadata.collectAsState() - val statPeriod by viewModel.statPeriod.collectAsState() + val indexChips by viewModel.indexChips.collectAsState() val mostPlayedSongs by viewModel.mostPlayedSongs.collectAsState() + val mostPlayedSongsStats by viewModel.mostPlayedSongsStats.collectAsState() val mostPlayedArtists by viewModel.mostPlayedArtists.collectAsState() val mostPlayedAlbums by viewModel.mostPlayedAlbums.collectAsState() + println("sui $mostPlayedAlbums") + val firstEvent by viewModel.firstEvent.collectAsState() + val currentDate = LocalDateTime.now() val coroutineScope = rememberCoroutineScope() + val selectedOption by viewModel.selectedOption.collectAsState() + + val weeklyDates = + if (currentDate != null && firstEvent != null) { + generateSequence(currentDate) { it.minusWeeks(1) } + .takeWhile { it.isAfter(firstEvent?.event?.timestamp?.minusWeeks(1)) } + .mapIndexed { index, date -> + val endDate = date.plusWeeks(1).minusDays(1).coerceAtMost(currentDate) + val formatter = DateTimeFormatter.ofPattern("dd MMM") + + val startDateFormatted = formatter.format(date) + val endDateFormatted = formatter.format(endDate) + + val startMonth = date.month + val endMonth = endDate.month + val startYear = date.year + val endYear = endDate.year + + val text = + when { + startYear != currentDate.year -> "$startDateFormatted, $startYear - $endDateFormatted, $endYear" + startMonth != endMonth -> "$startDateFormatted - $endDateFormatted" + else -> "${date.dayOfMonth} - $endDateFormatted" + } + Pair(index, text) + }.toList() + } else { + emptyList() + } + + val monthlyDates = + if (currentDate != null && firstEvent != null) { + generateSequence(currentDate.plusMonths(1).withDayOfMonth(1).minusDays(1)) { it.minusMonths(1) } + .takeWhile { + it.isAfter( + firstEvent + ?.event + ?.timestamp + ?.withDayOfMonth(1), + ) + }.mapIndexed { index, date -> + val formatter = DateTimeFormatter.ofPattern("MMM") + val formattedDate = formatter.format(date) + val text = + if (date.year != currentDate.year) { + "$formattedDate ${date.year}" + } else { + formattedDate + } + Pair(index, text) + }.toList() + } else { + emptyList() + } + + val yearlyDates = + if (currentDate != null && firstEvent != null) { + generateSequence( + currentDate + .plusYears(1) + .withDayOfYear(1) + .minusDays(1), + ) { it.minusYears(1) } + .takeWhile { + it.isAfter( + firstEvent + ?.event + ?.timestamp, + ) + }.mapIndexed { index, date -> + Pair(index, "${date.year}") + }.toList() + } else { + emptyList() + } + LazyColumn( contentPadding = LocalPlayerAwareWindowInsets.current @@ -76,99 +157,120 @@ fun StatsScreen( modifier = Modifier.windowInsetsPadding(LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top)), ) { item { - ChipsRow( + ChoiceChipsRow( chips = + when (selectedOption) { + OptionStats.WEEKS -> weeklyDates + OptionStats.MONTHS -> monthlyDates + OptionStats.YEARS -> yearlyDates + OptionStats.CONTINUOUS -> { + listOf( + StatPeriod.WEEK_1.ordinal to pluralStringResource(R.plurals.n_week, 1, 1), + StatPeriod.MONTH_1.ordinal to pluralStringResource(R.plurals.n_month, 1, 1), + StatPeriod.MONTH_3.ordinal to pluralStringResource(R.plurals.n_month, 3, 3), + StatPeriod.MONTH_6.ordinal to pluralStringResource(R.plurals.n_month, 6, 6), + StatPeriod.YEAR_1.ordinal to pluralStringResource(R.plurals.n_year, 1, 1), + StatPeriod.ALL.ordinal to stringResource(R.string.filter_all), + ) + } + }, + options = listOf( - StatPeriod.WEEK_1 to pluralStringResource(R.plurals.n_week, 1, 1), - StatPeriod.MONTH_1 to pluralStringResource(R.plurals.n_month, 1, 1), - StatPeriod.MONTH_3 to pluralStringResource(R.plurals.n_month, 3, 3), - StatPeriod.MONTH_6 to pluralStringResource(R.plurals.n_month, 6, 6), - StatPeriod.YEAR_1 to pluralStringResource(R.plurals.n_year, 1, 1), - StatPeriod.ALL to stringResource(R.string.filter_all), + OptionStats.CONTINUOUS to stringResource(id = R.string.continuous), + OptionStats.WEEKS to stringResource(R.string.weeks), + OptionStats.MONTHS to stringResource(R.string.months), + OptionStats.YEARS to stringResource(R.string.years), ), - currentValue = statPeriod, - onValueUpdate = { viewModel.statPeriod.value = it }, + selectedOption = selectedOption, + onSelectionChange = { + viewModel.selectedOption.value = it + viewModel.indexChips.value = 0 + }, + currentValue = indexChips, + onValueUpdate = { viewModel.indexChips.value = it }, ) } item(key = "mostPlayedSongs") { NavigationTitle( - title = stringResource(R.string.most_played_songs), - modifier = Modifier.animateItemPlacement(), + title = "${mostPlayedSongsStats.size} ${stringResource(id = R.string.songs)}", + modifier = Modifier.animateItem(), ) - } - items( - items = mostPlayedSongs, - key = { it.id }, - ) { song -> - SongListItem( - song = song, - isActive = song.id == mediaMetadata?.id, - isPlaying = isPlaying, - trailingContent = { - IconButton( - onClick = { - menuState.show { - SongMenu( - originalSong = song, - navController = navController, - onDismiss = menuState::dismiss, - ) - } - }, - ) { - Icon( - painter = painterResource(R.drawable.more_vert), - contentDescription = null, - ) - } - }, - modifier = - Modifier - .fillMaxWidth() - .combinedClickable( - onClick = { - if (song.id == mediaMetadata?.id) { - playerConnection.player.togglePlayPause() - } else { - playerConnection.playQueue( - YouTubeQueue( - endpoint = WatchEndpoint(song.id), - preloadItem = song.toMediaMetadata(), - ), - ) - } - }, - onLongClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - menuState.show { - SongMenu( - originalSong = song, - navController = navController, - onDismiss = menuState::dismiss, - ) - } - }, - ).animateItemPlacement(), - ) + LazyRow( + modifier = Modifier.animateItem(), + ) { + itemsIndexed( + items = mostPlayedSongsStats, + key = { _, song -> song.id }, + ) { index, song -> + LocalItemsGrid( + title = "${index + 1}. ${song.title}", + subtitle = + joinByBullet( + pluralStringResource( + R.plurals.n_time, + song.songCountListened, + song.songCountListened, + ), + makeTimeString(song.timeListened), + ), + thumbnailUrl = song.thumbnailUrl, + isActive = song.id == mediaMetadata?.id, + isPlaying = isPlaying, + modifier = + Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { + if (song.id == mediaMetadata?.id) { + playerConnection.player.togglePlayPause() + } else { + playerConnection.playQueue( + YouTubeQueue( + endpoint = WatchEndpoint(song.id), + preloadItem = mostPlayedSongs[index].toMediaMetadata(), + ), + ) + } + }, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + menuState.show { + SongMenu( + originalSong = mostPlayedSongs[index], + navController = navController, + onDismiss = menuState::dismiss, + ) + } + }, + ).animateItem(), + ) + } + } } item(key = "mostPlayedArtists") { NavigationTitle( - title = stringResource(R.string.most_played_artists), - modifier = Modifier.animateItemPlacement(), + title = "${mostPlayedArtists.size} ${stringResource(id = R.string.artists)}", + modifier = Modifier.animateItem(), ) LazyRow( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), ) { - items( + itemsIndexed( items = mostPlayedArtists, - key = { it.id }, - ) { artist -> - ArtistGridItem( - artist = artist, + key = { _, artist -> artist.id }, + ) { index, artist -> + LocalItemsGrid( + title = "${index + 1}. ${artist.artist.name}", + subtitle = + joinByBullet( + pluralStringResource(R.plurals.n_time, artist.songCount, artist.songCount), + makeTimeString(artist.timeListened?.toLong()), + ), + thumbnailUrl = artist.artist.thumbnailUrl, modifier = Modifier .fillMaxWidth() @@ -186,31 +288,36 @@ fun StatsScreen( ) } }, - ).animateItemPlacement(), + ).animateItem(), ) } } } - if (mostPlayedAlbums.isNotEmpty()) { - item(key = "mostPlayedAlbums") { - NavigationTitle( - title = stringResource(R.string.most_played_albums), - modifier = Modifier.animateItemPlacement(), - ) + item(key = "mostPlayedAlbums") { + NavigationTitle( + title = "${mostPlayedAlbums.size} ${stringResource(id = R.string.albums)}", + modifier = Modifier.animateItem(), + ) + if (mostPlayedAlbums.isNotEmpty()) { LazyRow( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), ) { - items( + itemsIndexed( items = mostPlayedAlbums, - key = { it.id }, - ) { album -> - AlbumGridItem( - album = album, + key = { _, album -> album.id }, + ) { index, album -> + LocalItemsGrid( + title = "${index + 1}. ${album.album.title}", + subtitle = + joinByBullet( + pluralStringResource(R.plurals.n_time, album.songCountListened!!, album.songCountListened), + makeTimeString(album.timeListened?.toLong()), + ), + thumbnailUrl = album.album.thumbnailUrl, isActive = album.id == mediaMetadata?.album?.id, isPlaying = isPlaying, - coroutineScope = coroutineScope, modifier = Modifier .fillMaxWidth() @@ -228,7 +335,7 @@ fun StatsScreen( ) } }, - ).animateItemPlacement(), + ).animateItem(), ) } } @@ -251,3 +358,10 @@ fun StatsScreen( }, ) } + +enum class OptionStats { + WEEKS, + MONTHS, + YEARS, + CONTINUOUS, +} diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt index 1c66f6580..673f636c7 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt @@ -137,7 +137,7 @@ fun LibrarySongsScreen( ) { if (selection) { val count = wrappedSongs.count { it.isSelected } - Text(text = pluralStringResource(R.plurals.n_elements, count, count), modifier = Modifier.weight(1f)) + Text(text = pluralStringResource(R.plurals.n_element, count, count), modifier = Modifier.weight(1f)) IconButton( onClick = { if (count == wrappedSongs.size) { diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/playlist/TopPlaylistScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/playlist/TopPlaylistScreen.kt index 8bc94390f..fa0dc22a0 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/playlist/TopPlaylistScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/playlist/TopPlaylistScreen.kt @@ -385,7 +385,7 @@ fun TopPlaylistScreen( ) { if (selection) { val count = wrappedSongs?.count { it.isSelected } ?: 0 - Text(text = pluralStringResource(R.plurals.n_elements, count, count), modifier = Modifier.weight(1f)) + Text(text = pluralStringResource(R.plurals.n_element, count, count), modifier = Modifier.weight(1f)) IconButton( onClick = { if (count == wrappedSongs?.size) { diff --git a/app/src/main/java/com/malopieds/innertune/viewmodels/StatsViewModel.kt b/app/src/main/java/com/malopieds/innertune/viewmodels/StatsViewModel.kt index d5d69e3ce..ba27c9b09 100644 --- a/app/src/main/java/com/malopieds/innertune/viewmodels/StatsViewModel.kt +++ b/app/src/main/java/com/malopieds/innertune/viewmodels/StatsViewModel.kt @@ -3,19 +3,22 @@ package com.malopieds.innertune.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.malopieds.innertube.YouTube -import com.malopieds.innertune.constants.StatPeriod +import com.malopieds.innertune.constants.statToPeriod import com.malopieds.innertune.db.MusicDatabase +import com.malopieds.innertune.ui.screens.OptionStats import com.malopieds.innertune.utils.reportException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.time.Duration import java.time.LocalDateTime +import java.time.ZoneOffset import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @@ -25,28 +28,106 @@ class StatsViewModel constructor( val database: MusicDatabase, ) : ViewModel() { - val statPeriod = MutableStateFlow(StatPeriod.`WEEK_1`) + val selectedOption = MutableStateFlow(OptionStats.CONTINUOUS) + val indexChips = MutableStateFlow(0) + + val mostPlayedSongsStats = + combine( + selectedOption, + indexChips, + ) { first, second -> Pair(first, second) } + .flatMapLatest { (selection, t) -> + database + .mostPlayedSongsStats( + fromTimeStamp = statToPeriod(selection, t), + limit = -1, + toTimeStamp = + if (selection == OptionStats.CONTINUOUS || t == 0) { + LocalDateTime + .now() + .toInstant( + ZoneOffset.UTC, + ).toEpochMilli() + } else { + statToPeriod(selection, t - 1) + }, + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) val mostPlayedSongs = - statPeriod - .flatMapLatest { period -> - database.mostPlayedSongs(period.toTimeMillis()) + combine( + selectedOption, + indexChips, + ) { first, second -> Pair(first, second) } + .flatMapLatest { (selection, t) -> + database + .mostPlayedSongs( + statToPeriod(selection, t), + toTimeStamp = + if (selection == OptionStats.CONTINUOUS || t == 0) { + LocalDateTime + .now() + .toInstant( + ZoneOffset.UTC, + ).toEpochMilli() + } else { + statToPeriod(selection, t - 1) + }, + ) }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) val mostPlayedArtists = - statPeriod - .flatMapLatest { period -> - database.mostPlayedArtists(period.toTimeMillis()).map { artists -> - artists.filter { it.artist.isYouTubeArtist } - } + combine( + selectedOption, + indexChips, + ) { first, second -> Pair(first, second) } + .flatMapLatest { (selection, t) -> + database + .mostPlayedArtists( + statToPeriod(selection, t), + limit = -1, + toTimeStamp = + if (selection == OptionStats.CONTINUOUS || t == 0) { + LocalDateTime + .now() + .toInstant( + ZoneOffset.UTC, + ).toEpochMilli() + } else { + statToPeriod(selection, t - 1) + }, + ).map { artists -> + artists.filter { it.artist.isYouTubeArtist } + } }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) val mostPlayedAlbums = - statPeriod - .flatMapLatest { period -> - database.mostPlayedAlbums(period.toTimeMillis()) + combine( + selectedOption, + indexChips, + ) { first, second -> Pair(first, second) } + .flatMapLatest { (selection, t) -> + database.mostPlayedAlbums( + statToPeriod(selection, t), + limit = -1, + toTimeStamp = + if (selection == OptionStats.CONTINUOUS || t == 0) { + LocalDateTime + .now() + .toInstant( + ZoneOffset.UTC, + ).toEpochMilli() + } else { + statToPeriod(selection, t - 1) + }, + ) }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + val firstEvent = + database + .firstEvent() + .stateIn(viewModelScope, SharingStarted.Lazily, null) + init { viewModelScope.launch { mostPlayedArtists.collect { artists -> diff --git a/app/src/main/res/values-DE/strings.xml b/app/src/main/res/values-DE/strings.xml index d44ea0be7..4ff9973bb 100644 --- a/app/src/main/res/values-DE/strings.xml +++ b/app/src/main/res/values-DE/strings.xml @@ -178,7 +178,7 @@ %d Jahr %d Jahre - + %d Element %d Elemente diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 174cb6a52..2465f7854 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -161,7 +161,7 @@ %d سنة %d من السنوات - + %d عنصر %d من العناصر diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 2e3bf5161..be20863b4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -192,7 +192,7 @@ %d let %d let - + %d prvek %d prvky %d prvků diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 6b1df997f..2c1d390fb 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -178,7 +178,7 @@ %d ans %d ans - + %d élément %d éléments %d éléments diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 1c0c9531a..0da076c60 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -178,7 +178,7 @@ %d年 - + %d 要素 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 225c235cb..a44a1a03d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -195,7 +195,7 @@ %d lat %d lat - + %d element %d elementy %d elementów diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c3ad8aab9..3c621f4c7 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -179,7 +179,7 @@ %d yıl %d yıl - + %d öğe %d öğe diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 208d30d5d..80b52c6b6 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,10 @@ Most played songs Most played artists Most played albums + Weeks + Months + Years + Continuous Search @@ -179,10 +183,14 @@ %d year %d years - + %d element %d elements + + %d time + %d times + Playlist imported From deb07cfd5220914cb27f4b3e7e607a8c4e795422 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 23 Aug 2024 21:13:19 +0800 Subject: [PATCH 09/14] fix: repetition in history, #35 --- .../innertune/viewmodels/HistoryViewModel.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/malopieds/innertune/viewmodels/HistoryViewModel.kt b/app/src/main/java/com/malopieds/innertune/viewmodels/HistoryViewModel.kt index 1d11f8e93..dc1a29c0c 100644 --- a/app/src/main/java/com/malopieds/innertune/viewmodels/HistoryViewModel.kt +++ b/app/src/main/java/com/malopieds/innertune/viewmodels/HistoryViewModel.kt @@ -47,18 +47,20 @@ class HistoryViewModel is DateAgo.Other -> ChronoUnit.DAYS.between(dateAgo.date, today) } }, - ) + ).mapValues { entry -> + entry.value.distinctBy { it.song.id } + } }.stateIn(viewModelScope, SharingStarted.Lazily, emptyMap()) } sealed class DateAgo { - object Today : DateAgo() + data object Today : DateAgo() - object Yesterday : DateAgo() + data object Yesterday : DateAgo() - object ThisWeek : DateAgo() + data object ThisWeek : DateAgo() - object LastWeek : DateAgo() + data object LastWeek : DateAgo() class Other( val date: LocalDate, From d89198079deca4e2ccd5f6f24f9ba5b8c92af263 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 23 Aug 2024 21:51:04 +0800 Subject: [PATCH 10/14] feat: click bottom nav item to scroll to top, #134 --- .../com/malopieds/innertune/MainActivity.kt | 17 ++++++++---- .../innertune/ui/screens/HomeScreen.kt | 12 +++++++++ .../ui/screens/library/LibraryAlbumsScreen.kt | 26 ++++++++++++++++--- .../screens/library/LibraryArtistsScreen.kt | 21 +++++++++++++++ .../ui/screens/library/LibraryMixScreen.kt | 17 ++++++++++++ .../screens/library/LibraryPlaylistsScreen.kt | 15 +++++++++++ .../ui/screens/library/LibrarySongsScreen.kt | 12 +++++++++ 7 files changed, 111 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/malopieds/innertune/MainActivity.kt b/app/src/main/java/com/malopieds/innertune/MainActivity.kt index 5f90c30c6..538be60f4 100644 --- a/app/src/main/java/com/malopieds/innertune/MainActivity.kt +++ b/app/src/main/java/com/malopieds/innertune/MainActivity.kt @@ -762,12 +762,19 @@ class MainActivity : ComponentActivity() { ) }, onClick = { - navController.navigate(screen.route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true + if (navBackStackEntry?.destination?.hierarchy?.any { it.route == screen.route } == true) { + navController.currentBackStackEntry?.savedStateHandle?.set("scrollToTop", true) + coroutineScope.launch { + searchBarScrollBehavior.state.resetHeightOffset() + } + } else { + navController.navigate(screen.route) { + popUpTo(navController.graph.startDestinationId) { + saveState = true + } + launchSingleTop = true + restoreState = true } - launchSingleTop = true - restoreState = true } }, ) diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/HomeScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/HomeScreen.kt index 10a138d85..4ef6a0548 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/HomeScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -45,6 +46,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.malopieds.innertube.models.AlbumItem @@ -140,6 +142,16 @@ fun HomeScreen( val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() + val backStackEntry by navController.currentBackStackEntryAsState() + val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow("scrollToTop", false)?.collectAsState() + + LaunchedEffect(scrollToTop?.value) { + if (scrollToTop?.value == true) { + scrollState.animateScrollTo(value = 0) + backStackEntry?.savedStateHandle?.set("scrollToTop", false) + } + } + SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing), onRefresh = viewModel::refresh, diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryAlbumsScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryAlbumsScreen.kt index af02a4a79..7aaceb3ef 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryAlbumsScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryAlbumsScreen.kt @@ -15,7 +15,9 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -23,6 +25,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -37,6 +40,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import com.malopieds.innertune.LocalPlayerAwareWindowInsets import com.malopieds.innertune.LocalPlayerConnection import com.malopieds.innertune.R @@ -85,6 +89,21 @@ fun LibraryAlbumsScreen( val coroutineScope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() + val lazyGridState = rememberLazyGridState() + val backStackEntry by navController.currentBackStackEntryAsState() + val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow("scrollToTop", false)?.collectAsState() + + LaunchedEffect(scrollToTop?.value) { + if (scrollToTop?.value == true) { + when (viewType) { + LibraryViewType.LIST -> lazyListState.animateScrollToItem(0) + LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0) + } + backStackEntry?.savedStateHandle?.set("scrollToTop", false) + } + } + val filterContent = @Composable { Row { Spacer(Modifier.width(12.dp)) @@ -169,6 +188,7 @@ fun LibraryAlbumsScreen( when (viewType) { LibraryViewType.LIST -> LazyColumn( + state = lazyListState, contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(), ) { item( @@ -236,10 +256,8 @@ fun LibraryAlbumsScreen( LibraryViewType.GRID -> LazyVerticalGrid( - columns = - GridCells.Adaptive( - minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp, - ), + state = lazyGridState, + columns = GridCells.Adaptive(minSize = GridThumbnailHeight + 24.dp), contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(), ) { item( diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryArtistsScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryArtistsScreen.kt index 8b4fce6be..e19fdbee9 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryArtistsScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryArtistsScreen.kt @@ -15,7 +15,9 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -23,6 +25,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -37,6 +40,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import com.malopieds.innertune.LocalPlayerAwareWindowInsets import com.malopieds.innertune.R import com.malopieds.innertune.constants.ArtistFilter @@ -80,6 +84,21 @@ fun LibraryArtistsScreen( val artists by viewModel.allArtists.collectAsState() val coroutineScope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() + val lazyGridState = rememberLazyGridState() + val backStackEntry by navController.currentBackStackEntryAsState() + val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow("scrollToTop", false)?.collectAsState() + + LaunchedEffect(scrollToTop?.value) { + if (scrollToTop?.value == true) { + when (viewType) { + LibraryViewType.LIST -> lazyListState.animateScrollToItem(0) + LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0) + } + backStackEntry?.savedStateHandle?.set("scrollToTop", false) + } + } + val filterContent = @Composable { Row { Spacer(Modifier.width(12.dp)) @@ -161,6 +180,7 @@ fun LibraryArtistsScreen( when (viewType) { LibraryViewType.LIST -> LazyColumn( + state = lazyListState, contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(), ) { item( @@ -226,6 +246,7 @@ fun LibraryArtistsScreen( LibraryViewType.GRID -> LazyVerticalGrid( + state = lazyGridState, columns = GridCells.Adaptive( minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp, diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryMixScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryMixScreen.kt index c906609a7..d81b6f753 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryMixScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryMixScreen.kt @@ -17,9 +17,11 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -33,6 +35,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import com.malopieds.innertune.LocalPlayerAwareWindowInsets import com.malopieds.innertune.LocalPlayerConnection import com.malopieds.innertune.R @@ -152,7 +155,20 @@ fun LibraryMixScreen( val coroutineScope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() val lazyGridState = rememberLazyGridState() + val backStackEntry by navController.currentBackStackEntryAsState() + val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow("scrollToTop", false)?.collectAsState() + + LaunchedEffect(scrollToTop?.value) { + if (scrollToTop?.value == true) { + when (viewType) { + LibraryViewType.LIST -> lazyListState.animateScrollToItem(0) + LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0) + } + backStackEntry?.savedStateHandle?.set("scrollToTop", false) + } + } val headerContent = @Composable { Row( verticalAlignment = Alignment.CenterVertically, @@ -200,6 +216,7 @@ fun LibraryMixScreen( when (viewType) { LibraryViewType.LIST -> LazyColumn( + state = lazyListState, contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(), ) { item( diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryPlaylistsScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryPlaylistsScreen.kt index b95e157e4..26bccf901 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryPlaylistsScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibraryPlaylistsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -39,6 +40,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import com.malopieds.innertune.LocalDatabase import com.malopieds.innertune.LocalPlayerAwareWindowInsets import com.malopieds.innertune.R @@ -111,6 +113,19 @@ fun LibraryPlaylistsScreen( val lazyListState = rememberLazyListState() val lazyGridState = rememberLazyGridState() + val backStackEntry by navController.currentBackStackEntryAsState() + val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow("scrollToTop", false)?.collectAsState() + + LaunchedEffect(scrollToTop?.value) { + if (scrollToTop?.value == true) { + when (viewType) { + LibraryViewType.LIST -> lazyListState.animateScrollToItem(0) + LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0) + } + backStackEntry?.savedStateHandle?.set("scrollToTop", false) + } + } + var showAddPlaylistDialog by rememberSaveable { mutableStateOf(false) } diff --git a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt index 673f636c7..9db2e1a31 100644 --- a/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt +++ b/app/src/main/java/com/malopieds/innertune/ui/screens/library/LibrarySongsScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import com.malopieds.innertune.LocalPlayerAwareWindowInsets import com.malopieds.innertune.LocalPlayerConnection import com.malopieds.innertune.R @@ -89,6 +91,16 @@ fun LibrarySongsScreen( val lazyListState = rememberLazyListState() + val backStackEntry by navController.currentBackStackEntryAsState() + val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow("scrollToTop", false)?.collectAsState() + + LaunchedEffect(scrollToTop?.value) { + if (scrollToTop?.value == true) { + lazyListState.animateScrollToItem(0) + backStackEntry?.savedStateHandle?.set("scrollToTop", false) + } + } + Box( modifier = Modifier.fillMaxSize(), ) { From bcc9cda3bd71cb53e322464663ba75b1212b8890 Mon Sep 17 00:00:00 2001 From: Michael Moroni Date: Sat, 24 Aug 2024 11:10:02 +0200 Subject: [PATCH 11/14] Update strings.xml (#287) * Update strings.xml Updated Italian translation * Added some missing keys * Translated some untranslated strings * Changed some translations * One key cannot be translated ("filter_bookmarked") as I don't know the context * Update Italian strings.xml As suggested by @lucasmz-dev I removed the direct mention to albums in a string --- app/src/main/res/values-it/strings.xml | 98 +++++++++++++++++--------- 1 file changed, 64 insertions(+), 34 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 27b45bf1b..64e19e074 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -5,23 +5,27 @@ Artisti Album Playlist - + Esplora + %d selezionato %d selezionati - %d selezionati Cronologia Statistiche Mood e generi - Account + Profilo Scelte rapide Ascolta alcuni brani per generare le tue scelte rapide - Nuovi album in uscita - + Nuove uscite + Preferiti dimenticati + Simile a + Continua ad ascoltare + Le tue playlist di YouTube + Oggi Ieri @@ -37,9 +41,9 @@ Cerca Cerca su YouTube Music… Cerca nella libreria… - Library - Liked - Downloaded + Libreria + Piaciuti + Scaricati Tutto Brani Video @@ -48,17 +52,27 @@ Playlist Playlist della community Playlist in rilievo - Bookmarked + Bookmarked Nessun risultato trovato Dalla tua libreria + + Altre versioni + + + Piaciuti + Scaricati + I miei migliori + Brani piaciuti Brani scaricati La playlist è vuota - + Vuoi veramente rimuovere tutti i brani scaricati dalla playlist \"%s\"? + Vuoi veramente rimuovere la playlist \"%s\"? + Riprova Radio @@ -87,14 +101,17 @@ Rimuovi da cronologia Cerca online Sincronizza - Advanced - + Advanzato + Seleziona tutto + Mi piace tutto + Data di aggiunta + Data di aggiornamento Nome Artista Anno - Numero brani + Numero di brani Durata Numero di riproduzioni Ordine personalizzato @@ -126,7 +143,8 @@ Crea playlist Nome della playlist Il nome della playlist non può essere vuoto. - + Già nella playlist: + Modifica artista Nome dell\'artista Il nome dell\'artista non può essere vuoto. @@ -135,39 +153,36 @@ %d brano %d brani - %d brani %d artista %d artisti - %d artisti %d album %d album - %d album %d playlist %d playlist - %d playlist %d settimana %d settimane - %d settimane %d mese %d mesi - %d mesi %d anno %d anni - %d anni - + + %d elemento + %d elementi + + Playlist importata Rimosso \"%s\" da playlist @@ -181,7 +196,6 @@ 1 minuto %d minuti - %d minuti Stream non disponibile Nessuna connessione di rete @@ -190,19 +204,19 @@ Mi piace - Rimuovi mi piace - Shuffle on - Shuffle off - Repeat mode off - Repeat current song - Repeat queue + Non mi piace più + Modalità casuale attivata + Modalità casuale disattivata + Ripetizione disattivata + Ripeti attuale brano + Ripeti coda Tutti i brani Brani cercati - Riproduttore + Lettore Impostazioni @@ -212,6 +226,10 @@ Attivato Disattivato Segui sistema + Stile dello sfondo del lettore + Segui tema + Gradiente + Abilita cursore ondulato Nero Scheda principale predefinita Personalizza le schede di navigazione @@ -225,9 +243,13 @@ Lingua predefinita dei contenuti Paese predefinito dei contenuti Predefinito di sistema + Proxy Attiva proxy Tipologia del proxy URL proxy + Cambia linguetta predefinita della libreria + Imposta le scelte rapide + Basate sugli ultimi brani ascoltati Riavvia l\'app per applicare le modifiche Riproduttore e audio @@ -261,7 +283,8 @@ Pulisci la cronologia delle ricerche Sei sicuro di voler cancellare la cronologia delle ricerche? Attiva i testi forniti da KuGou - + Attiva i testi forniti da LrcLib + Backup e ripristino Backup Ripristina @@ -273,7 +296,14 @@ Informazioni Versione dell\'app - New version available - Translation Models - Clear translation models + Nuova versione disponibile + Modelli di traduzione + Cancella modelli di traduzione + + Da sempre + Ultime 24 ore + Ultima settimana + Ultimo mese + Ultimo anno + Lunghezza della lista dei miei migliori From 19d38b71f086857177e9c07414b21c4ef28c8a23 Mon Sep 17 00:00:00 2001 From: Malopieds Date: Sat, 24 Aug 2024 19:53:54 +0200 Subject: [PATCH 12/14] fix: building --- app/src/main/res/values-it/strings.xml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 64e19e074..ce804cc34 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -11,6 +11,7 @@ %d selezionato %d selezionati + %d selezionati @@ -153,36 +154,39 @@ %d brano %d brani + %d brani %d artista %d artisti + %d artisti %d album %d album + %d album %d playlist %d playlist + %d playlist %d settimana %d settimane + %d settimane %d mese %d mesi + %d mesi %d anno %d anni + %d anni - - %d elemento - %d elementi - - + Playlist importata Rimosso \"%s\" da playlist From 940d3d02d763becadff5fe8d7d043559e5bfcd84 Mon Sep 17 00:00:00 2001 From: Dimitrys_AF Date: Sat, 24 Aug 2024 21:02:02 +0300 Subject: [PATCH 13/14] Add Greek localization and updated some plugins/libs versions (#229) * Create strings.xml Greek language added * Update strings.xml Fixed a plural issue * Update strings.xml plural change * Search online >> Search Didn't fit * Update "..." to "(insert smaller dots here)" --------- Co-authored-by: Malopieds <74783202+Malopieds@users.noreply.github.com> --- app/src/main/res/values-el-rGR/strings.xml | 312 +++++++++++++++++++++ gradle/libs.versions.toml | 10 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 318 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/values-el-rGR/strings.xml diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml new file mode 100644 index 000000000..c5745357e --- /dev/null +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -0,0 +1,312 @@ + + + Αρχική + Τραγούδια + Καλλιτέχνες + Άλμπουμ + Playlists + Εξερεύνηση + + + + επιλέχτηκε 1 + επιλέχθηκαν %d + + + + Ιστορία + Στατιστικά + Διάθεση και Είδη + Λογαριασμός + Γρήγορη επιλογή + Ακούστε τραγούδια για να δημιουργήσετε τις γρήγορες επιλογές + Νέες κυκλοφορίες + Ξεχασμένα + Παρόμοια με + Συνέχισε να ακούς + Το YouTube playlist σου + + + Σήμερα + Εχθές + Αυτήν την εβδομάδα + Την προηγούμενη εβδομάδα + + + Τραγούδια που παίχτηκαν περισσότερο + Καλλιτέχνες που παίχτηκαν περισσότερο + Άλμπουμ που παίχτηκαν περισσότερο + + + Αναζήτηση + Αναζήτηση στο YouTube Μουσική… + Αναζήτηση βιβλιοθήκης… + Βιβλιοθήκη + Αγαπημένα + Κατεβασμένα + Όλα + Τραγούδια + Βίντεο + Άλμπουμ + Καλλιτέχνες + Playlists + Playlists κοινότητας + Επιλεγμένα playlists + Σελιδοδείκτη + Κανένα αποτέλεσμα + + + Από την βιβλιοθήκη σας + + + Άλλες Εκδόσεις + + + Αγαπημένα + Κατεβασμένα + Τα Top μου + + + + Αγαπημένα τραγούδια + Κατεβασμένα τραγούδια + Το playlist είναι άδειο + Θέλετε να καταργήσετε όλα τα τραγούδια του playlist \"%s\" από τον χώρο αποθήκευσης Κατεβασμένων Τραγουδιών; + Θέλετε να διαγράψετε το playlist \"%s\"; + + + Επαναδοκιμή + Ράδιο + Ανάμιξη + Επαναφορά + + + Λεπτομέρειες + Επεξεργασία + Έναρξη ραδίου + Αναπαραγωγή + Παίξε το επόμενο + Προσθήκη στην ουρά + Προσθήκη στην βιβλιοθήκη + Αφαίρεση από την βιβλιοθήκη + Λήψη + Κατεβαίνει + Αφαίρεση κατεβασμένου + Εισαγωγή playlist + Προσθήκη σε playlist + Προβολή καλλιτέχνη + Προβολή άλμπουμ + Επανάκτηση + Κοινοποίηση + Διαγραφή + Διαγραφή από την ιστορία + Αναζήτηση + Συγχρονισμό + Προχωρημένο + επιλογή όλων + Αγαπημένα όλα + + + Ημερομηνία προσθήκης + Ημερομηνία ενημέρωσης + Όνομα + Καλλιτέχνες + Χρόνος + Πλήθος τραγουδιών + Μέγεθος + Χρόνος αναπαραγωγής + Προσαρμοσμένη σειρά + + + Id μέσου + Τύπος MIME + Κωδικοποιητές + Bitrate + Ρυθμός δειγματοληψίας + Ηχηρότητα + Ένταση ήχου + Μέγεθος αρχείου + Άγνωστο + Αντιγράφηκε στο πρόχειρο + + Επεξεργασία στίχων + Αναζήτηση στίχων + + Επεξεργασία τραγουδιού + Τίτλος τραγουδιού + Καλλιτέχνης τραγουδιού + Ο τίτλος τραγουδιού δεν μπορεί να είναι άδειος. + Ο καλλιτέχνης τραγουδιού δεν μπορεί να είναι άδειος. + Αποθήκευση + + Επιλογή playlist + Επεξεργασία playlist + Δημιουργία playlist + Όνομα playlist + Το όνομα του playlist δεν μπορεί να είναι άδειο. + Ήδη στο playlist: + + Επεξεργασία καλλιτέχνη + Όνομα καλλιτέχνη + Το όνομα του καλλιτέχνη δεν μπορεί να είναι άδειο. + + + + %d τραγούδι + %d τραγούδια + + + %d καλλιτέχνης + %d καλλιτέχνες + + + %d άλμπουμ + %d άλμπουμ + + + %d playlist + %d playlists + + + %d εβδομάδα + %d εβδομάδες + + + %d μήνας + %d μήνες + + + %d χρόνος + %d χρόνια + + + %d στοιχείο + %d στοιχεία + + + + Το playlist εισαχθεί + Αφαιρέθηκε το \"%s\" απο το playlist + Έγινε συγχρονισμός playlist + Αναίρεση + + + Δεν βρέθηκαν στίχοι + Χρονόμετρο ύπνου + Τέλος τραγουδιού + + 1 λεπτό + %d λεπτά + + Δεν υπάρχει διαθέσιμη ροή + Δεν έχετε σύνδεση στο διαδίκτυο + Τέλος χρόνου + Άγνωστο σφάλμα + + + Μου αρέσει + Αφαίρεση μου αρέσει + Ανάμιξη ενεργή + Ανάμιξη ανενεργή + Επανάληψη ενεργή + Επανάληψη τρέχοντος τραγουδιού + Επανάληψη ροής + + + Όλα τα τραγούδια + Τραγούδια που αναζητήθηκαν + + + Αναπαραγωγή Μουσικής + + + Ρυθμίσεις + Εμφάνιση + Ενεργοποίηση δυναμικού θέματος + Σκούρο θέμα + Ενεργό + Ανενεργό + Συστήματος + Στυλ φόντου αναπαραγωγητή + Συστήματος + Βαθμίδα + Ενεργοποίηση κυματιστών γραμμών + Καθαρό μαύρο + Προεπιλεγμένη ανοιχτή ταμπέλα + Προσαρμογή καρτέλας εξερεύνησης + Θέση στίχων + Αριστερά + Κέντρο + Δεξιά + + Περιεχόμενο + Σύνδεση + Προεπιλεγμένη γλώσσα περιεχομένου + Προεπιλεγμένη χώρα περιεχομένου + Συστήματος + Proxy + Ενεργοποίηση proxy + Τύπος proxy + Σύνδεσμος Proxy + Αλλαγή προεπιλεγμένων τσιπ βιβλιοθήκης + Ορισμό Γρήγορων Επιλογών + Βασισμένο στο τελευταίο τραγούδι που ακούσατε + Επανεκκίνηση για εφαρμογή αλλαγών + + Αναπαραγωγητής και ήχος + Ποιότητα ήχου + Αυτόματο + Υψηλή + Χαμηλή + Μόνιμη ουρά + Παραλήψεις σιωπής + Κανονικοποίηση ήχου + Εξισωτής + + Αποθήκευση + Προσωρινή Μνήμη + Προσωρινή Μνήμη Φωτογραφίων + Προσωρινή Μνήμη Τραγουδιών + Μέγιστη προσωρινή μνήμη + Απεριόριστη + Εκκαθάριση όλων των κατεβασμένων + Μέγιστη προσωρινή μνήμη φωτογραφιών + Εκκαθάριση προσωρινής μνήμης + Μέγιστη προσωρινής μνήμης τραγουδιών + Εκκαθάριση προσωρινής μνήμης τραγουδιών + %s χρησιμοποιούνται + + Απόρρητο + Παύση ιστορικού ακρόασης + Εκκαθάριση ιστορικού ακρόασης + Είστε σίγουροι να διαγραφεί όλο το ιστορικό ακρόασης; + Παύση ιστορικού αναζήτησης + Εκκαθάριση ιστορικού αναζήτησης + Είστε σίγουροι να διαγραφεί όλο το ιστορικό αναζήτησης; + Ενεργοποίηση στίχων KuGou + Ενεργοποίηση στίχων LrcLib + + Αντίγραφο και επαναφορά + Αντίγραφο ασφάλειας + Επαναφορά + Εισαχθεί playlist + Αντιγράφο ασφαλείας δημιουργήθηκε επιτυχώς + Δεν ήταν δυνατή η δημιουργία αντιγράφου ασφαλείας + Αποτυχία επαναφοράς + + Σχετικά + Έκδοση εφαρμογής + + Μια νέα έκδοση είναι διαθέσιμη + Μοντέλα Μετάφρασης + Εκκαθάριση μοντέλων μετάφρασης + + Συνεχώς + Τελευταίες 24 ώρες + Τελευταία εβδομάδα + Τελευταίο μήνα + Τελευταίο χρόνο + Το μήκος της Top λίστας μου + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d0cd7e41a..3ef01e806 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ hilt-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", v datastore = { group = "androidx.datastore", name = "datastore-preferences", version = "1.1.1" } compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "compose" } -compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version = "compose" } +compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version = "1.6.8" } compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" } compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "compose" } compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" } @@ -41,11 +41,11 @@ viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel- material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } -accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version = "0.28.0" } +accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version = "0.34.0" } coil = { group = "io.coil-kt", name = "coil-compose", version = "2.6.0" } -shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version = "1.0.3" } +shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version = "1.3.0" } palette = { group = "androidx.palette", name = "palette", version = "1.0.0" } @@ -57,7 +57,7 @@ room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = " room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } -apache-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = "3.12.0" } +apache-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = "3.14.0" } hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } @@ -73,7 +73,7 @@ json = { module = "org.json:json", version.ref = "json" } brotli = { group = "org.brotli", name = "dec", version = "0.1.2" } -opencc4j = { group = "com.github.houbb", name = "opencc4j", version = "1.7.2" } +opencc4j = { group = "com.github.houbb", name = "opencc4j", version = "1.8.1" } desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version = "2.0.4" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48c0a02ca..a8382d7c4 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file From cb8bd68e8b4a064345d8eeee1f4cf09a6c066b53 Mon Sep 17 00:00:00 2001 From: Malopieds Date: Sat, 24 Aug 2024 20:11:44 +0200 Subject: [PATCH 14/14] fix: building --- app/src/main/res/values-el-rGR/strings.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index c5745357e..e34881dfc 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -179,10 +179,6 @@ %d χρόνος %d χρόνια - - %d στοιχείο - %d στοιχεία - Το playlist εισαχθεί