diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt index 7fd9f81c..aed9bc07 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/MainActivity.kt @@ -94,6 +94,9 @@ class MainActivity : ComponentActivity() { onFeedListClick = { navController.navigate(Screen.FeedList.name) }, + onAddFeedClick = { + navController.navigate(Screen.AddFeed.name) + }, navigateBack = { navController.popBackStack() }, diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/settings/SettingsScreen.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/settings/SettingsScreen.kt index f2e304f1..428b6324 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/settings/SettingsScreen.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/settings/SettingsScreen.kt @@ -50,6 +50,7 @@ import org.koin.compose.koinInject @Composable fun SettingsScreen( onFeedListClick: () -> Unit, + onAddFeedClick: () -> Unit, navigateBack: () -> Unit, onAboutClick: () -> Unit, navigateToImportExport: () -> Unit, @@ -69,6 +70,7 @@ fun SettingsScreen( SettingsScreenContent( browsers = browserListState, onFeedListClick = onFeedListClick, + onAddFeedClick = onAddFeedClick, isMarkReadWhenScrollingEnabled = settingState.isMarkReadWhenScrollingEnabled, onBrowserSelected = { browser -> browserManager.setFavouriteBrowser(browser) @@ -92,11 +94,13 @@ fun SettingsScreen( ) } +@Suppress("LongParameterList") @Composable private fun SettingsScreenContent( browsers: List, isMarkReadWhenScrollingEnabled: Boolean, onFeedListClick: () -> Unit, + onAddFeedClick: () -> Unit, onBrowserSelected: (Browser) -> Unit, navigateBack: () -> Unit, onAboutClick: () -> Unit, @@ -132,6 +136,7 @@ private fun SettingsScreenContent( modifier = Modifier .padding(paddingValues), onFeedListClick = onFeedListClick, + onAddFeedClick = onAddFeedClick, onBrowserSelectionClick = { showBrowserSelection = true }, @@ -175,6 +180,7 @@ private fun SettingsList( isMarkReadWhenScrollingEnabled: Boolean, modifier: Modifier = Modifier, onFeedListClick: () -> Unit, + onAddFeedClick: () -> Unit, onBrowserSelectionClick: () -> Unit, navigateToImportExport: () -> Unit, onAboutClick: () -> Unit, @@ -198,9 +204,9 @@ private fun SettingsList( item { SettingsMenuItem( - text = stringResource(resource = MR.strings.browser_selection_button), + text = stringResource(resource = MR.strings.add_feed), ) { - onBrowserSelectionClick() + onAddFeedClick() } } @@ -220,6 +226,18 @@ private fun SettingsList( SettingsDivider() } + item { + SettingsMenuItem( + text = stringResource(resource = MR.strings.browser_selection_button), + ) { + onBrowserSelectionClick() + } + } + + item { + SettingsDivider() + } + item { MarkReadWhenScrollingSwitch( setMarkReadWhenScrolling = setMarkReadWhenScrolling, @@ -299,6 +317,7 @@ private fun SettingsScreenPreview() { browsers = browsersForPreview, isMarkReadWhenScrollingEnabled = true, onFeedListClick = {}, + onAddFeedClick = {}, onBrowserSelected = {}, navigateBack = {}, onAboutClick = {}, diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/settings/about/AboutScreen.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/settings/about/AboutScreen.kt index 6faea9c3..b1ee2122 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/settings/about/AboutScreen.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/settings/about/AboutScreen.kt @@ -2,6 +2,7 @@ package com.prof18.feedflow.settings.about import FeedFlowTheme import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons @@ -9,6 +10,7 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -17,7 +19,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign import com.prof18.feedflow.BrowserManager +import com.prof18.feedflow.BuildConfig import com.prof18.feedflow.MR import com.prof18.feedflow.core.utils.Websites.FEED_FLOW_WEBSITE import com.prof18.feedflow.core.utils.Websites.MG_WEBSITE @@ -56,6 +60,7 @@ fun AboutScreen( } @OptIn(ExperimentalMaterial3Api::class) +@Suppress("LongMethod") @Composable private fun AboutScreenContent( licensesClicked: () -> Unit, @@ -110,6 +115,17 @@ private fun AboutScreenContent( buttonText = stringResource(MR.strings.open_source_licenses), ) } + + item { + Text( + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onBackground, + text = stringResource(MR.strings.about_app_version, BuildConfig.VERSION_NAME), + style = MaterialTheme.typography.bodySmall, + ) + } } AuthorText( modifier = Modifier.align(Alignment.CenterHorizontally), diff --git a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/CategoriesState.kt b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/CategoriesState.kt index fc7875ac..c05201e3 100644 --- a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/CategoriesState.kt +++ b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/CategoriesState.kt @@ -8,7 +8,7 @@ data class CategoriesState( data class CategoryItem( val id: Long, - val name: String, + val name: String?, val isSelected: Boolean, val onClick: (CategoryId) -> Unit, ) diff --git a/database/src/commonMain/kotlin/com/prof18/feedflow/database/DatabaseHelper.kt b/database/src/commonMain/kotlin/com/prof18/feedflow/database/DatabaseHelper.kt index 7b14fb62..8b87edca 100644 --- a/database/src/commonMain/kotlin/com/prof18/feedflow/database/DatabaseHelper.kt +++ b/database/src/commonMain/kotlin/com/prof18/feedflow/database/DatabaseHelper.kt @@ -152,9 +152,11 @@ class DatabaseHelper( is FeedFilter.Category -> { dbRef.feedItemQueries.markAllReadByCategory(feedFilter.feedCategory.id) } + is FeedFilter.Source -> { dbRef.feedItemQueries.markAllReadByFeedSource(feedFilter.feedSource.id) } + FeedFilter.Timeline -> { dbRef.feedItemQueries.markAllRead() } @@ -225,6 +227,12 @@ class DatabaseHelper( .mapToOneOrDefault(0, backgroundDispatcher) .flowOn(backgroundDispatcher) + suspend fun deleteCategory(id: Long) = + dbRef.transactionWithContext(backgroundDispatcher) { + dbRef.feedSourceQueries.resetCategory(categoryId = id) + dbRef.feedSourceCategoryQueries.delete(id = id) + } + private suspend fun Transacter.transactionWithContext( coroutineContext: CoroutineContext, noEnclosing: Boolean = false, diff --git a/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSource.sq b/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSource.sq index 406f6f5e..9a36d6c5 100644 --- a/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSource.sq +++ b/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSource.sq @@ -39,3 +39,8 @@ updateLogoUrl: UPDATE feed_source SET logo_url = :logoUrl WHERE url_hash = :urlHash; + +resetCategory: +UPDATE feed_source +SET category_id = NULL +WHERE category_id = :categoryId; diff --git a/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSourceCategory.sq b/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSourceCategory.sq index 7be2a993..fe4d60e6 100644 --- a/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSourceCategory.sq +++ b/database/src/commonMain/sqldelight/com/prof18/feedflow/db/FeedSourceCategory.sq @@ -12,4 +12,7 @@ SELECT * FROM feed_source_category WHERE title = ?; selectAll: SELECT * FROM feed_source_category -ORDER BY title COLLATE NOCASE ASC; \ No newline at end of file +ORDER BY title COLLATE NOCASE ASC; + +delete: +DELETE FROM feed_source_category WHERE id = ?; \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index e03aa972..c604c164 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -19,7 +19,7 @@ Import done Export done Settings - Favorite web browser + Browser Import feed from OPML Export feeds to OPML About @@ -45,6 +45,7 @@ Force feeds refresh The link you provided is not a valid RSS feed Import and export OPML + Import and export Retry OK The following feeds were not added because they are not valid @@ -55,14 +56,19 @@ Issue with FeedFlow Please describe the issue and provide a link to the RSS Feed that causes the issues. If you can, please attach a screenshot and the OPML file There was an error while getting the title of the feed - No category selected + No category New category name Uncategorized Timeline Categories Feed Sources - Mark items as read when scrolling + Mark as read when scrolling Done General App + App Version: %s + Category + Categories + Save + FeedFlow supports the import and export of your subscriptions with an OPML file, for better interoperability with the other feed readers \ No newline at end of file diff --git a/iosApp/Source/Settings/About/AboutScreen.swift b/iosApp/Source/Settings/About/AboutScreen.swift index 09585f01..930492fa 100644 --- a/iosApp/Source/Settings/About/AboutScreen.swift +++ b/iosApp/Source/Settings/About/AboutScreen.swift @@ -15,54 +15,59 @@ struct AboutScreen: View { @Environment(\.openURL) private var openURL - @State - private var showLicensesSheet = false - - @State - private var licensesContent: String = "" - var body: some View { - VStack { - Text(localizer.about_the_app.localized) - .padding(Spacing.regular) - .font(.system(size: 16)) + List { + Section { + Text(localizer.about_the_app.localized) + .padding(.vertical, Spacing.small) + .font(.system(size: 16)) - Button( - localizer.open_website_button.localized, - action: { - if let url = URL(string: Websites.shared.FEED_FLOW_WEBSITE) { - self.openURL(url) + NavigationLink(destination: LicensesScreen()) { + Label( + localizer.open_source_licenses.localized, + systemImage: "shield" + ) } - } - ) - .buttonStyle(.bordered) - .padding(.top, Spacing.regular) - - Button( - localizer.open_source_licenses.localized, - action: { - let baseURL = Bundle.main.url(forResource: "licenses", withExtension: "html")! - let htmlString = try? String(contentsOf: baseURL, encoding: String.Encoding.utf8) - self.licensesContent = htmlString ?? "" - self.showLicensesSheet.toggle() + Link(destination: URL(string: Websites.shared.FEED_FLOW_WEBSITE)!) { + Label( + localizer.open_website_button.localized, + systemImage: "globe" + ) + } + } footer: { + if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + let appVersionString = LocalizationUtils.shared.formatString( + resource: MR.strings().about_app_version, + args: [appVersion] + ) + Text(appVersionString) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, Spacing.small) + } } - ) - .buttonStyle(.bordered) - .padding(.top, Spacing.regular) + } + .listStyle(.insetGrouped) + .scrollContentBackground(.hidden) + .padding(.top, -Spacing.medium) + .background(Color.secondaryBackgroundColor) Spacer() let authorLink: LocalizedStringKey = """ - \(localizer.author_label.localized) [Marco Gomiero](https://www.marcogomiero.com) - """ + \(localizer.author_label.localized) [Marco Gomiero](https://www.marcogomiero.com) + """ Text(authorLink) - }.sheet(isPresented: $showLicensesSheet) { - LicensesScreen(htmlContent: licensesContent) + .padding(.bottom, Spacing.small) } - .navigationTitle(localizer.about_nav_bar.localized) + .background(Color.secondaryBackgroundColor) + .navigationTitle(Text(localizer.about_nav_bar.localized)) .navigationBarTitleDisplayMode(.inline) - } } + +#Preview { + AboutScreen() + +} diff --git a/iosApp/Source/Settings/About/LicensesScreen.swift b/iosApp/Source/Settings/About/LicensesScreen.swift index e21aac51..f159dd28 100644 --- a/iosApp/Source/Settings/About/LicensesScreen.swift +++ b/iosApp/Source/Settings/About/LicensesScreen.swift @@ -14,35 +14,34 @@ struct LicensesScreen: View { @Environment(\.presentationMode) private var presentationMode - let htmlContent: String + @State + var htmlContent: String? var body: some View { - NavigationStack { - HTMLStringView( - htmlContent: htmlContent - ) + licenseView .padding(Spacing.regular) - .toolbar { - - ToolbarItem(placement: .navigationBarLeading) { - Button( - action: { - self.presentationMode.wrappedValue.dismiss() - }, - label: { - Image(systemName: "xmark") - } - ) - } - - ToolbarItem(placement: .navigationBarLeading) { - Text(localizer.open_source_nav_bar.localized) - .font(.title2) - .padding(.vertical, Spacing.medium) - } + .onAppear { + let baseURL = Bundle.main.url(forResource: "licenses", withExtension: "html")! + let htmlString = try? String(contentsOf: baseURL, encoding: String.Encoding.utf8) + + self.htmlContent = htmlString ?? "" } + } + + @ViewBuilder + private var licenseView: some View { + if let content = htmlContent { + HTMLStringView( + htmlContent: content + ) + } else { + Spacer() + + ProgressView() + Spacer() } } + } struct LicensesScreen_Previews: PreviewProvider { diff --git a/iosApp/Source/Settings/AddFeed/AddFeedScreen.swift b/iosApp/Source/Settings/AddFeed/AddFeedScreen.swift index cb9f2212..0cf50d32 100644 --- a/iosApp/Source/Settings/AddFeed/AddFeedScreen.swift +++ b/iosApp/Source/Settings/AddFeed/AddFeedScreen.swift @@ -18,6 +18,12 @@ struct AddFeedScreen: View { @Environment(\.presentationMode) private var presentationMode + @StateObject + private var addFeedViewModel: AddFeedViewModel = KotlinDependencies.shared.getAddFeedViewModel() + + @StateObject + private var categorySelectorObserver = CategorySelectorObserver() + @State private var feedURL = "" @@ -27,117 +33,72 @@ struct AddFeedScreen: View { @State private var errorMessage = "" - @State - private var isCategoriesSelectorExpanded = false - - @State - private var headerMessage = localizer.no_category_selected_header.localized - @State private var categoryItems: [CategoriesState.CategoryItem] = [] @State - private var newCategoryName = "" + private var newCategory: String = "" - @StateObject - private var addFeedViewModel: AddFeedViewModel = KotlinDependencies.shared.getAddFeedViewModel() + @State + private var isAddingFeed: Bool = false var body: some View { - VStack(alignment: .leading) { - TextField( - localizer.feed_url.localized, - text: $feedURL - ) - .keyboardType(.webSearch) - .border(showError ? Color.red : Color.clear) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .padding(.top, Spacing.regular) - .padding(.horizontal, Spacing.regular) - - if showError { - Text(errorMessage) - .padding(.horizontal, Spacing.regular) - .frame(alignment: .leading) - .font(.caption) - .foregroundColor(.red) - } - - DisclosureGroup( - isExpanded: $isCategoriesSelectorExpanded, + Form { + Section( content: { - ForEach(categoryItems, id: \.self.id) { categoryItem in - HStack { - RadioButtonView( - title: categoryItem.name, - isSelected: categoryItem.isSelected, - onRadioSelected: { - withAnimation { - isCategoriesSelectorExpanded.toggle() - } - categoryItem.onClick(CategoryId(value: categoryItem.id)) - } - ) - - Spacer() - } - } - .padding(.top, Spacing.small) - - HStack { - TextField( - localizer.new_category_hint.localized, - text: $newCategoryName - ) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - Button( - action: { - addFeedViewModel.addNewCategory( - categoryName: CategoryName(name: newCategoryName) - ) - newCategoryName = "" - }, - label: { - Image(systemName: "plus") - } - ) - .disabled(newCategoryName.isEmpty) - } - .padding(.top, Spacing.regular) + TextField( + localizer.feed_url.localized, + text: $feedURL + ) }, - label: { - Text(headerMessage) + header: { + Text(localizer.feed_url.localized) + }, + footer: { + if showError { + Text(errorMessage) + .font(.caption) + .foregroundColor(.red) + } } ) - .padding(Spacing.regular) - Button( - action: { - addFeedViewModel.addFeed() - }, - label: { - Text(localizer.add_feed.localized) - .frame(maxWidth: .infinity) + Section(localizer.add_feed_category_title.localized) { + Picker( + selection: $categorySelectorObserver.selectedCategory, + label: Text(localizer.add_feed_categories_title.localized) + ) { + ForEach(categoryItems, id: \.self.id) { categoryItem in + let title = categoryItem.name ?? localizer.no_category_selected_header.localized + Text(title).tag(categoryItem as CategoriesState.CategoryItem?) + } } - ) - .disabled(feedURL.isEmpty) - .buttonStyle(.bordered) - .padding(.horizontal, Spacing.regular) + } - Spacer() + if !categoryItems.isEmpty { + categoriesSection + } } + .scrollContentBackground(.hidden) + .scrollDismissesKeyboard(.interactively) + .background(Color.secondaryBackgroundColor) .navigationTitle(localizer.add_feed.localized) .navigationBarTitleDisplayMode(.inline) .onChange(of: feedURL) { value in addFeedViewModel.updateFeedUrlTextFieldValue(feedUrlTextFieldValue: value) } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + saveButton + } + } .task { do { let stream = asyncSequence(for: addFeedViewModel.feedAddedState) for try await state in stream { switch state { case let addedState as FeedAddedState.FeedAdded: -// self.addFeedViewModel.clearAddDoneState() + // self.addFeedViewModel.clearAddDoneState() self.appState.snackbarQueue.append( SnackbarData( title: addedState.message.localized(), @@ -153,6 +114,7 @@ struct AddFeedScreen: View { case let errorState as FeedAddedState.Error: errorMessage = errorState.errorMessage.localized() + isAddingFeed = false showError = true default: @@ -167,14 +129,7 @@ struct AddFeedScreen: View { do { let stream = asyncSequence(for: addFeedViewModel.categoriesState) for try await state in stream { - isCategoriesSelectorExpanded = state.isExpanded - - if let header = state.header { - self.headerMessage = header - } else { - self.headerMessage = localizer.no_category_selected_header.localized - } - + self.categorySelectorObserver.selectedCategory = state.categories.first { $0.isSelected } self.categoryItems = state.categories } } catch { @@ -182,27 +137,73 @@ struct AddFeedScreen: View { } } } -} -private struct RadioButtonView: View { - var title: String - var isSelected: Bool - var onRadioSelected: () -> Void + private var categoriesSection: some View { + Section(localizer.add_feed_categories_title.localized) { + ForEach(categoryItems, id: \.self.id) { categoryItem in + if let name = categoryItem.name { + + HStack { + Text(name) + Spacer() + Button { + addFeedViewModel.deleteCategory(categoryId: categoryItem.id) + } label: { + Image(systemName: "trash") + .tint(.red) + } + } + } + } - var body: some View { - Button(action: onRadioSelected) { HStack { - Image(systemName: isSelected ? "largecircle.fill.circle" : "circle") - Text(title) + TextField(localizer.new_category_hint.localized, text: $newCategory, axis: .horizontal) + .onSubmit { + addFeedViewModel.addNewCategory(categoryName: CategoryName(name: newCategory)) + newCategory = "" + } + Spacer() + if !newCategory.isEmpty { + Button { + addFeedViewModel.addNewCategory(categoryName: CategoryName(name: newCategory)) + newCategory = "" + } label: { + Image(systemName: "checkmark.circle.fill") + .tint(.green) + } + } + } + } + } + + private var saveButton: some View { + Button { + Task { + isAddingFeed.toggle() + addFeedViewModel.addFeed() + } + } label: { + if isAddingFeed { + ProgressView() + } else { + Text(localizer.action_save.localized).bold() } } - .foregroundColor(.primary) - .padding(.vertical, 8) + .disabled(feedURL.isEmpty) } + } -struct SwiftUIView_Previews: PreviewProvider { - static var previews: some View { - AddFeedScreen() +class CategorySelectorObserver: ObservableObject { + @Published var selectedCategory: CategoriesState.CategoryItem? { + didSet { + if let selectedCategory = selectedCategory { + selectedCategory.onClick(CategoryId(value: selectedCategory.id)) + } + } } } + +#Preview { + AddFeedScreen() +} diff --git a/iosApp/Source/Settings/Feeds/FeedSourceListScreen.swift b/iosApp/Source/Settings/Feeds/FeedSourceListScreen.swift index 1d2d18c6..d738b22f 100644 --- a/iosApp/Source/Settings/Feeds/FeedSourceListScreen.swift +++ b/iosApp/Source/Settings/Feeds/FeedSourceListScreen.swift @@ -59,37 +59,38 @@ private struct FeedSourceListContent: View { let deleteFeedSource: (FeedSource) -> Void var body: some View { - VStack { - if feedState.isEmpty() { - VStack { - Spacer() + VStack { + if feedState.isEmpty() { + VStack { + Spacer() - Text(localizer.no_feeds_found_message.localized) - .font(.body) + Text(localizer.no_feeds_found_message.localized) + .font(.body) - NavigationLink(destination: AddFeedScreen()) { - Text(localizer.add_feed.localized) - } - - Spacer() + NavigationLink(destination: AddFeedScreen()) { + Text(localizer.add_feed.localized) } - } else { - if feedState.feedSourcesWithoutCategory.count > 0 { - List { - ForEach(feedState.feedSourcesWithoutCategory, id: \.self.id) { feedSource in - FeedSourceListItem(feedSource: feedSource) - } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + + if feedState.feedSourcesWithoutCategory.count > 0 { + List { + ForEach(feedState.feedSourcesWithoutCategory, id: \.self.id) { feedSource in + FeedSourceListItem(feedSource: feedSource) } - .padding(.top, -Spacing.medium) } + .padding(.top, -Spacing.medium) + } - List { - ForEach(feedState.feedSourcesWithCategory, id: \.self.categoryId) { feedSourceState in - DisclosureGroup( - content: { - ForEach(feedSourceState.feedSources, id: \.self.id) { feedSource in - FeedSourceListItem(feedSource: feedSource) + List { + ForEach(feedState.feedSourcesWithCategory, id: \.self.categoryId) { feedSourceState in + DisclosureGroup( + content: { + ForEach(feedSourceState.feedSources, id: \.self.id) { feedSource in + FeedSourceListItem(feedSource: feedSource) .padding(.trailing, Spacing.small) .id(feedSource.id) .contextMenu { @@ -102,62 +103,45 @@ private struct FeedSourceListContent: View { ) } } - } - }, - label: { - Text(feedSourceState.categoryName ?? localizer.no_category.localized) - .font(.system(size: 16)) - .foregroundStyle(Color(UIColor.label)) - .bold() - .padding(Spacing.regular) } - ) - .listRowInsets( - EdgeInsets( - top: .zero, - leading: .zero, - bottom: .zero, - trailing: Spacing.small) - ) - } - } - .padding(.top, -Spacing.medium) - .sheet(isPresented: $showAddFeed) { - AddFeedScreen() + }, + label: { + Text(feedSourceState.categoryName ?? localizer.no_category.localized) + .font(.system(size: 16)) + .foregroundStyle(Color(UIColor.label)) + .padding(Spacing.regular) + } + ) + .listRowInsets( + EdgeInsets( + top: .zero, + leading: .zero, + bottom: .zero, + trailing: Spacing.regular) + ) } } - - Spacer() - } - .scrollContentBackground(.hidden) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button( - action: { - self.presentationMode.wrappedValue.dismiss() - }, - label: { - Image(systemName: "xmark") - } - ) - .padding(.leading, Spacing.small) - + .padding(.top, -Spacing.medium) + .sheet(isPresented: $showAddFeed) { + AddFeedScreen() } + } - ToolbarItem(placement: .navigationBarLeading) { - Text(localizer.feeds_title.localized) - .font(.title2) - .padding(.vertical, Spacing.medium) + Spacer() + } + .scrollContentBackground(.hidden) + .background(Color.secondaryBackgroundColor) + .navigationTitle(Text(localizer.feeds_title.localized)) + .toolbar { + ToolbarItem(placement: .primaryAction) { + NavigationLink(destination: AddFeedScreen()) { + Image(systemName: "plus") } + .buttonStyle(.bordered) + .padding(.trailing, Spacing.small) - ToolbarItem(placement: .primaryAction) { - NavigationLink(destination: AddFeedScreen()) { - Image(systemName: "plus") - } - .padding(.trailing, Spacing.small) - - } } + } } } @@ -201,30 +185,26 @@ struct FeedSourceListItem: View { } } -struct FeedSourceListContent_Previews: PreviewProvider { - static var previews: some View { - FeedSourceListContent( - feedState: .constant( - FeedSourceListState( - feedSourcesWithoutCategory: [], - feedSourcesWithCategory: PreviewItemsKt.feedSourcesState - ) - ), - deleteFeedSource: { _ in } - ) - } +#Preview { + FeedSourceListContent( + feedState: .constant( + FeedSourceListState( + feedSourcesWithoutCategory: [], + feedSourcesWithCategory: PreviewItemsKt.feedSourcesState + ) + ), + deleteFeedSource: { _ in } + ) } -struct FeedSourceListContentEmpty_Previews: PreviewProvider { - static var previews: some View { - FeedSourceListContent( - feedState: .constant( - FeedSourceListState( - feedSourcesWithoutCategory: [], - feedSourcesWithCategory: [] - ) - ), - deleteFeedSource: { _ in } - ) - } +#Preview { + FeedSourceListContent( + feedState: .constant( + FeedSourceListState( + feedSourcesWithoutCategory: [], + feedSourcesWithCategory: [] + ) + ), + deleteFeedSource: { _ in } + ) } diff --git a/iosApp/Source/Settings/ImportExport/ImportExportScreen.swift b/iosApp/Source/Settings/ImportExport/ImportExportScreen.swift index e4adc7eb..b3789421 100644 --- a/iosApp/Source/Settings/ImportExport/ImportExportScreen.swift +++ b/iosApp/Source/Settings/ImportExport/ImportExportScreen.swift @@ -43,7 +43,7 @@ struct ImportExportScreen: View { viewModel.clearState() } ) - .navigationTitle(localizer.import_export_opml.localized) + .navigationTitle(localizer.import_export_opml_title.localized) .navigationBarTitleDisplayMode(.inline) .task { do { @@ -149,7 +149,7 @@ struct ImportExportContent: View { ) default: - Text("Aronne") + EmptyView() } } @@ -163,6 +163,13 @@ struct ImportExportIdleView: View { var body: some View { VStack { Form { + + Section { + Text(localizer.import_export_description.localized) + .font(.body) + .multilineTextAlignment(.leading) + } + Button( action: onImportClick, label: { diff --git a/iosApp/Source/Settings/SettingsScreen.swift b/iosApp/Source/Settings/SettingsScreen.swift index a3176394..55a3dcf5 100644 --- a/iosApp/Source/Settings/SettingsScreen.swift +++ b/iosApp/Source/Settings/SettingsScreen.swift @@ -117,10 +117,10 @@ private struct SettingsContent: View { ) Toggle(isOn: $isMarkReadWhenScrollingEnabled) { - Label( - localizer.toggle_mark_read_when_scrolling.localized, - systemImage: "envelope.open" - ) + Label( + localizer.toggle_mark_read_when_scrolling.localized, + systemImage: "envelope.open" + ) } } } @@ -128,10 +128,6 @@ private struct SettingsContent: View { @ViewBuilder private var appSection: some View { Section(localizer.settings_app_title.localized) { - NavigationLink(destination: AboutScreen()) { - Label(localizer.about_button.localized, systemImage: "info.circle") - } - Button( action: { let subject = localizer.issue_content_title.localized @@ -153,6 +149,10 @@ private struct SettingsContent: View { ) } ) + + NavigationLink(destination: AboutScreen()) { + Label(localizer.about_button.localized, systemImage: "info.circle") + } } } diff --git a/shared/src/commonMain/kotlin/com/prof18/feedflow/domain/feed/manager/FeedManagerRepository.kt b/shared/src/commonMain/kotlin/com/prof18/feedflow/domain/feed/manager/FeedManagerRepository.kt index 2f8dfb13..6d975a50 100644 --- a/shared/src/commonMain/kotlin/com/prof18/feedflow/domain/feed/manager/FeedManagerRepository.kt +++ b/shared/src/commonMain/kotlin/com/prof18/feedflow/domain/feed/manager/FeedManagerRepository.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withContext +@Suppress("TooManyFunctions") internal class FeedManagerRepository( private val databaseHelper: DatabaseHelper, private val opmlFeedHandler: OpmlFeedHandler, @@ -125,4 +126,8 @@ internal class FeedManagerRepository( null } } + + suspend fun deleteCategory(categoryId: Long) { + databaseHelper.deleteCategory(categoryId) + } } diff --git a/shared/src/commonMain/kotlin/com/prof18/feedflow/presentation/AddFeedViewModel.kt b/shared/src/commonMain/kotlin/com/prof18/feedflow/presentation/AddFeedViewModel.kt index 140edef8..16fb8b41 100644 --- a/shared/src/commonMain/kotlin/com/prof18/feedflow/presentation/AddFeedViewModel.kt +++ b/shared/src/commonMain/kotlin/com/prof18/feedflow/presentation/AddFeedViewModel.kt @@ -104,11 +104,20 @@ class AddFeedViewModel internal constructor( } } + fun deleteCategory(categoryId: Long) { + scope.launch { + feedManagerRepository.deleteCategory(categoryId) + } + } + private fun getSelectedCategory(): FeedSourceCategory? { - val category = categoriesState.value.categories.firstOrNull { it.isSelected } ?: return null + val category = categoriesState.value.categories.firstOrNull { it.isSelected } + if (category == null || category.id == EMPTY_CATEGORY_ID) { + return null + } return FeedSourceCategory( id = category.id, - title = category.name, + title = requireNotNull(category.name), ) } @@ -148,16 +157,30 @@ class AddFeedViewModel internal constructor( private fun initCategories() { scope.launch { feedManagerRepository.observeCategories().collect { categories -> + val categoriesWithEmpty = listOf(getEmptyCategory()) + categories.map { feedSourceCategory -> + feedSourceCategory.toCategoryItem() + } val categoriesState = CategoriesState( isExpanded = false, header = newCategoryName?.name, - categories = categories.map { feedSourceCategory -> - feedSourceCategory.toCategoryItem() - }, + categories = categoriesWithEmpty, ) categoriesMutableState.update { categoriesState } newCategoryName = null } } } + + private fun getEmptyCategory() = CategoriesState.CategoryItem( + id = EMPTY_CATEGORY_ID, + name = null, + isSelected = newCategoryName?.name == null, + onClick = { categoryId -> + onCategorySelected(categoryId) + }, + ) + + private companion object { + private const val EMPTY_CATEGORY_ID = Long.MAX_VALUE + } } diff --git a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/addfeed/CategoriesSelector.kt b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/addfeed/CategoriesSelector.kt index da1bc6bf..691f9ddf 100644 --- a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/addfeed/CategoriesSelector.kt +++ b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/addfeed/CategoriesSelector.kt @@ -129,7 +129,7 @@ private fun CategoriesList( onClick = null, ) Text( - text = category.name, + text = category.name ?: stringResource(MR.strings.no_category_selected_header), color = MaterialTheme.colorScheme.onPrimaryContainer, ) Spacer(Modifier.weight(1f)) diff --git a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/importexport/ImportExportComponents.kt b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/importexport/ImportExportComponents.kt index 146da152..dfacca11 100644 --- a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/importexport/ImportExportComponents.kt +++ b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/ui/importexport/ImportExportComponents.kt @@ -105,7 +105,7 @@ private fun ImportExportNavBar(navigateBack: () -> Unit) { TopAppBar( title = { Text( - stringResource(resource = MR.strings.import_export_opml), + stringResource(resource = MR.strings.import_export_opml_title), ) }, navigationIcon = {