diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/android/FeedFlowApp.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/android/FeedFlowApp.kt index 3e35ecf2..73712424 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/android/FeedFlowApp.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/android/FeedFlowApp.kt @@ -5,7 +5,6 @@ import android.content.Context import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner -import com.prof18.feedflow.android.readermode.ReaderModeViewModel import com.prof18.feedflow.core.utils.AppConfig import com.prof18.feedflow.core.utils.AppEnvironment import com.prof18.feedflow.shared.di.getWith @@ -14,7 +13,6 @@ import com.prof18.feedflow.shared.domain.feedsync.FeedSyncRepository import com.prof18.feedflow.shared.ui.utils.coilImageLoader import org.koin.android.ext.android.inject import org.koin.androidx.workmanager.koin.workManagerFactory -import org.koin.core.module.dsl.viewModel import org.koin.dsl.module class FeedFlowApp : Application() { @@ -69,12 +67,6 @@ class FeedFlowApp : Application() { ) } single { appConfig } - viewModel { - ReaderModeViewModel( - readerModeExtractor = get(), - settingsRepository = get(), - ) - } }, ), ) diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/android/MainActivity.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/android/MainActivity.kt index fd05f700..db57415f 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/android/MainActivity.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/android/MainActivity.kt @@ -41,16 +41,17 @@ import com.prof18.feedflow.android.editfeed.toFeedSource import com.prof18.feedflow.android.feedsourcelist.FeedSourceListScreen import com.prof18.feedflow.android.home.HomeScreen import com.prof18.feedflow.android.readermode.ReaderModeScreen -import com.prof18.feedflow.android.readermode.ReaderModeViewModel import com.prof18.feedflow.android.search.SearchScreen import com.prof18.feedflow.android.settings.SettingsScreen import com.prof18.feedflow.android.settings.about.AboutScreen import com.prof18.feedflow.android.settings.about.LicensesScreen import com.prof18.feedflow.android.settings.importexport.ImportExportScreen +import com.prof18.feedflow.core.model.FeedItemId import com.prof18.feedflow.core.model.FeedSource import com.prof18.feedflow.shared.domain.feedsync.FeedSyncMessageQueue import com.prof18.feedflow.shared.domain.model.SyncResult import com.prof18.feedflow.shared.presentation.EditFeedViewModel +import com.prof18.feedflow.shared.presentation.ReaderModeViewModel import com.prof18.feedflow.shared.ui.utils.LocalFeedFlowStrings import com.prof18.feedflow.shared.ui.utils.ProvideFeedFlowStrings import com.prof18.feedflow.shared.ui.utils.rememberFeedFlowStrings @@ -243,6 +244,9 @@ class MainActivity : ComponentActivity() { onUpdateFontSize = { newFontSize -> readerModeViewModel.updateFontSize(newFontSize) }, + onBookmarkClick = { feedItemId: FeedItemId, isBookmarked: Boolean -> + readerModeViewModel.updateBookmarkStatus(feedItemId, isBookmarked) + }, ) } diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/android/readermode/ReaderModeScreen.android.kt b/androidApp/src/main/kotlin/com/prof18/feedflow/android/readermode/ReaderModeScreen.android.kt index 2ffff17f..d96d01b6 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/android/readermode/ReaderModeScreen.android.kt +++ b/androidApp/src/main/kotlin/com/prof18/feedflow/android/readermode/ReaderModeScreen.android.kt @@ -20,6 +20,7 @@ import com.multiplatform.webview.web.WebViewNavigator import com.multiplatform.webview.web.rememberWebViewNavigator import com.multiplatform.webview.web.rememberWebViewStateWithHTMLData import com.prof18.feedflow.android.BrowserManager +import com.prof18.feedflow.core.model.FeedItemId import com.prof18.feedflow.core.model.ReaderModeState import com.prof18.feedflow.shared.domain.ReaderColors import com.prof18.feedflow.shared.domain.getReaderModeStyledHtml @@ -31,6 +32,7 @@ internal fun ReaderModeScreen( readerModeState: ReaderModeState, fontSize: Int, onUpdateFontSize: (Int) -> Unit, + onBookmarkClick: (FeedItemId, Boolean) -> Unit, navigateBack: () -> Unit, ) { val browserManager = koinInject() @@ -80,6 +82,7 @@ internal fun ReaderModeScreen( navigator = navigator, ) }, + onBookmarkClick = onBookmarkClick, ) } diff --git a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/FeedItemUrlInfo.kt b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/FeedItemUrlInfo.kt index 3e9949d2..cc21275e 100644 --- a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/FeedItemUrlInfo.kt +++ b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/FeedItemUrlInfo.kt @@ -5,4 +5,5 @@ data class FeedItemUrlInfo( val url: String, val title: String?, val openOnlyOnBrowser: Boolean = false, + val isBookmarked: Boolean, ) diff --git a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeData.kt b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeData.kt index 72ee19d1..90438280 100644 --- a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeData.kt +++ b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeData.kt @@ -1,8 +1,10 @@ package com.prof18.feedflow.core.model data class ReaderModeData( + val id: FeedItemId, val title: String?, val content: String, val url: String, val fontSize: Int, + val isBookmarked: Boolean, ) diff --git a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeState.kt b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeState.kt index f3029a9e..280d29b3 100644 --- a/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeState.kt +++ b/core/src/commonMain/kotlin/com/prof18/feedflow/core/model/ReaderModeState.kt @@ -3,5 +3,16 @@ package com.prof18.feedflow.core.model sealed interface ReaderModeState { data object Loading : ReaderModeState data class Success(val readerModeData: ReaderModeData) : ReaderModeState - data class HtmlNotAvailable(val url: String) : ReaderModeState + data class HtmlNotAvailable( + val url: String, + val id: String, + val isBookmarked: Boolean, + ) : ReaderModeState + + val getIsBookmarked: Boolean + get() = when (this) { + is Loading -> false + is Success -> readerModeData.isBookmarked + is HtmlNotAvailable -> isBookmarked + } } diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index fbc45b9a..02d3e5a1 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -48,7 +48,6 @@ kotlin { implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) - implementation(libs.flexmark.html2md.converter) implementation(libs.multiplatform.markdown.renderer.m3) implementation(libs.multiplatform.markdown.renderer.coil) implementation(libs.voyager.navigator) diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/di/DI.kt b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/di/DI.kt index f43b30e3..9ee9caaf 100644 --- a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/di/DI.kt +++ b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/di/DI.kt @@ -3,15 +3,11 @@ package com.prof18.feedflow.desktop.di import coil3.PlatformContext import com.prof18.feedflow.core.utils.AppEnvironment import com.prof18.feedflow.desktop.BrowserManager -import com.prof18.feedflow.desktop.reaadermode.MarkdownToHtmlConverter -import com.prof18.feedflow.desktop.reaadermode.ReaderModeViewModel import com.prof18.feedflow.desktop.versionchecker.NewVersionChecker import com.prof18.feedflow.shared.di.getWith import com.prof18.feedflow.shared.di.initKoinDesktop import com.prof18.feedflow.shared.ui.utils.coilImageLoader -import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter import org.koin.core.Koin -import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module object DI { @@ -36,15 +32,6 @@ object DI { ) } - single { - MarkdownToHtmlConverter( - converter = FlexmarkHtmlConverter.builder().build(), - dispatcherProvider = get(), - ) - } - - factoryOf(::ReaderModeViewModel) - factory { BrowserManager( settingsRepository = get(), diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/ReaderModeScreen.desktop.kt b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/ReaderModeScreen.desktop.kt index 9ff00ffd..79eee6a8 100644 --- a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/ReaderModeScreen.desktop.kt +++ b/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/ReaderModeScreen.desktop.kt @@ -25,10 +25,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.m3.markdownTypography +import com.prof18.feedflow.core.model.FeedItemId import com.prof18.feedflow.core.model.FeedItemUrlInfo import com.prof18.feedflow.desktop.desktopViewModel import com.prof18.feedflow.desktop.di.DI import com.prof18.feedflow.desktop.openInBrowser +import com.prof18.feedflow.shared.presentation.ReaderModeViewModel import com.prof18.feedflow.shared.ui.readermode.ReaderModeContent import com.prof18.feedflow.shared.ui.style.Spacing import com.prof18.feedflow.shared.ui.utils.LocalFeedFlowStrings @@ -78,8 +80,6 @@ internal data class ReaderModeScreen( }, fontSize = fontSize, onFontSizeChange = { - - readerModeViewModel.updateFontSize(it) }, readerModeSuccessView = { contentPadding, successState -> @@ -88,7 +88,7 @@ internal data class ReaderModeScreen( .padding(contentPadding) .verticalScroll(rememberScrollState()), - ) { + ) { Text( modifier = Modifier .fillMaxWidth() @@ -118,6 +118,9 @@ internal data class ReaderModeScreen( ) } }, + onBookmarkClick = { feedItemId: FeedItemId, isBookmarked: Boolean -> + readerModeViewModel.updateBookmarkStatus(feedItemId, isBookmarked) + }, ) } } diff --git a/iosApp/Source/App/CompactView.swift b/iosApp/Source/App/CompactView.swift index 3338af1c..4a81686f 100644 --- a/iosApp/Source/App/CompactView.swift +++ b/iosApp/Source/App/CompactView.swift @@ -6,218 +6,244 @@ // Copyright © 2023 FeedFlow. All rights reserved. // -import Foundation -import SwiftUI import FeedFlowKit +import Foundation import Reeeed +import SwiftUI struct CompactView: View { - @Environment(AppState.self) private var appState - @Environment(BrowserSelector.self) private var browserSelector - @Environment(\.openURL) private var openURL - - @Binding var selectedDrawerItem: DrawerItem? - - @State var navDrawerState: NavDrawerState = NavDrawerState( - timeline: [], - read: [], - bookmarks: [], - categories: [], - feedSourcesWithoutCategory: [], - feedSourcesByCategory: [:] - ) - @State var scrollUpTrigger: Bool = false - @State var showAddFeedSheet = false - @State var showEditFeedSheet = false - - @State var isToggled: Bool = false - @State private var showFontSizeMenu: Bool = false - @State private var fontSize = 16.0 - @State private var isSliderMoving = false - @State private var reset = false - - @StateObject private var vmStoreOwner = VMStoreOwner(Deps.shared.getReaderModeViewModel()) - - @State private var browserToOpen: BrowserToPresent? - - @State var indexHolder: HomeListIndexHolder - let homeViewModel: HomeViewModel - - @State private var feedSourceToEdit: FeedSource? - - var body: some View { - @Bindable var appState = appState - NavigationStack(path: $appState.compatNavigationPath) { - SidebarDrawer( - selectedDrawerItem: $selectedDrawerItem, - navDrawerState: navDrawerState, - onFeedFilterSelected: { feedFilter in - indexHolder.clear() - appState.navigate(route: CompactViewRoute.feed) - scrollUpTrigger.toggle() - homeViewModel.onFeedFilterSelected(selectedFeedFilter: feedFilter) - }, - onMarkAllReadClick: { - // On compact view it's handled by the home - }, - onDeleteOldFeedClick: { - // On compact view it's handled by the home - }, - onForceRefreshClick: { - // On compact view it's handled by the home - }, - deleteAllFeeds: { - // On compact view it's handled by the home - }, - onShowSettingsClick: { - // On compact view it's handled by the home - }, - onAddFeedClick: { - showAddFeedSheet.toggle() - }, - onEditFeedClick: { feedSource in - feedSourceToEdit = feedSource - showEditFeedSheet.toggle() - }, - onDeleteFeedClick: { feedSource in - homeViewModel.deleteFeedSource(feedSource: feedSource) + @Environment(AppState.self) private var appState + @Environment(BrowserSelector.self) private var browserSelector + @Environment(\.openURL) private var openURL + + @Binding var selectedDrawerItem: DrawerItem? + + @State var navDrawerState: NavDrawerState = NavDrawerState( + timeline: [], + read: [], + bookmarks: [], + categories: [], + feedSourcesWithoutCategory: [], + feedSourcesByCategory: [:] + ) + @State var scrollUpTrigger: Bool = false + @State var showAddFeedSheet = false + @State var showEditFeedSheet = false + + @State var isToggled: Bool = false + @State private var showFontSizeMenu: Bool = false + @State private var fontSize = 16.0 + @State private var isSliderMoving = false + @State private var reset = false + @State private var isBookmarked = false + + @StateObject private var vmStoreOwner = VMStoreOwner( + Deps.shared.getReaderModeViewModel()) + + @State private var browserToOpen: BrowserToPresent? + + @State var indexHolder: HomeListIndexHolder + let homeViewModel: HomeViewModel + + @State private var feedSourceToEdit: FeedSource? + + var body: some View { + @Bindable var appState = appState + NavigationStack(path: $appState.compatNavigationPath) { + SidebarDrawer( + selectedDrawerItem: $selectedDrawerItem, + navDrawerState: navDrawerState, + onFeedFilterSelected: { feedFilter in + indexHolder.clear() + appState.navigate(route: CompactViewRoute.feed) + scrollUpTrigger.toggle() + homeViewModel.onFeedFilterSelected(selectedFeedFilter: feedFilter) + }, + onMarkAllReadClick: { + // On compact view it's handled by the home + }, + onDeleteOldFeedClick: { + // On compact view it's handled by the home + }, + onForceRefreshClick: { + // On compact view it's handled by the home + }, + deleteAllFeeds: { + // On compact view it's handled by the home + }, + onShowSettingsClick: { + // On compact view it's handled by the home + }, + onAddFeedClick: { + showAddFeedSheet.toggle() + }, + onEditFeedClick: { feedSource in + feedSourceToEdit = feedSource + showEditFeedSheet.toggle() + }, + onDeleteFeedClick: { feedSource in + homeViewModel.deleteFeedSource(feedSource: feedSource) + } + ).sheet(isPresented: $showAddFeedSheet) { + AddFeedScreen(showCloseButton: true) + } + .sheet(isPresented: $showEditFeedSheet) { + if let feedSource = feedSourceToEdit { + EditFeedScreen(feedSource: feedSource) + } + } + .navigationDestination(for: CompactViewRoute.self) { route in + switch route { + case .feed: + HomeScreen( + toggleListScroll: $scrollUpTrigger, + showSettings: .constant(false), + selectedDrawerItem: $selectedDrawerItem, + homeViewModel: homeViewModel + ) + .environment(indexHolder) + } + } + .navigationDestination(for: CommonViewRoute.self) { route in + switch route { + case .readerMode(let feedItem): + Group { + ReeeederView( + url: URL(string: feedItem.url)!, + options: ReeeederViewOptions( + theme: .init( + additionalCSS: """ + #__reader_container { + font-size: \(fontSize)px + } + """ + ), + onLinkClicked: { url in + if browserSelector.openInAppBrowser() { + browserToOpen = .inAppBrowser(url: url) + } else { + openURL(browserSelector.getUrlForDefaultBrowser(stringUrl: url.absoluteString)) + } } - ).sheet(isPresented: $showAddFeedSheet) { - AddFeedScreen(showCloseButton: true) - } - .sheet(isPresented: $showEditFeedSheet) { - if let feedSource = feedSourceToEdit { - EditFeedScreen(feedSource: feedSource) + ), + toolbarContent: { + Button { + isBookmarked.toggle() + vmStoreOwner.instance.updateBookmarkStatus( + feedItemId: FeedItemId(id: feedItem.id), + bookmarked: isBookmarked + ) + } label: { + if isBookmarked { + Image(systemName: "bookmark.slash") + } else { + Image(systemName: "bookmark") + } } - } - .navigationDestination(for: CompactViewRoute.self) { route in - switch route { - case .feed: - HomeScreen( - toggleListScroll: $scrollUpTrigger, - showSettings: .constant(false), - selectedDrawerItem: $selectedDrawerItem, - homeViewModel: homeViewModel - ) - .environment(indexHolder) + + ShareLink( + item: URL(string: feedItem.url)!, + label: { + Label("Share", systemImage: "square.and.arrow.up") + }) + + Button { + if browserSelector.openInAppBrowser() { + browserToOpen = .inAppBrowser(url: URL(string: feedItem.url)!) + } else { + openURL( + browserSelector.getUrlForDefaultBrowser( + stringUrl: URL(string: feedItem.url)!.absoluteString)) + } + } label: { + Image(systemName: "globe") } + + fontSizeMenu + } + ) + .onAppear { + isBookmarked = feedItem.isBookmarked } - .navigationDestination(for: CommonViewRoute.self) { route in - switch route { - case .readerMode(let url): - ReeeederView( - url: url, - options: ReeeederViewOptions( - theme: .init( - additionalCSS: """ - #__reader_container { - font-size: \(fontSize)px - } - """ - ), - onLinkClicked: { url in - if browserSelector.openInAppBrowser() { - browserToOpen = .inAppBrowser(url: url) - } else { - openURL(browserSelector.getUrlForDefaultBrowser(stringUrl: url.absoluteString)) - } - } - ), - toolbarContent: { - Button { - if browserSelector.openInAppBrowser() { - browserToOpen = .inAppBrowser(url: url) - } else { - openURL(browserSelector.getUrlForDefaultBrowser(stringUrl: url.absoluteString)) - } - } label: { - Image(systemName: "globe") - } - - ShareLink(item: url) { - Label("Share", systemImage: "square.and.arrow.up") - } - fontSizeMenu - } - ) - .id(reset) + .id(reset) + } - case .search: - SearchScreen() + case .search: + SearchScreen() - case .accounts: - AccountsScreen() + case .accounts: + AccountsScreen() - case .dropboxSync: - DropboxSyncScreen() - } - } - .fullScreenCover(item: $browserToOpen) { browserToOpen in - switch browserToOpen { - case .inAppBrowser(let url): - SFSafariView(url: url) - .ignoresSafeArea() - } - } + case .dropboxSync: + DropboxSyncScreen() } - .navigationBarTitleDisplayMode(.inline) - .task { - for await state in homeViewModel.navDrawerState { - self.navDrawerState = state - } - }.task { - for await state in vmStoreOwner.instance.readerFontSizeState { - self.fontSize = Double(truncating: state) - } + } + .fullScreenCover(item: $browserToOpen) { browserToOpen in + switch browserToOpen { + case .inAppBrowser(let url): + SFSafariView(url: url) + .ignoresSafeArea() } + } } - - @ViewBuilder - private var fontSizeMenu: some View { - Button { - showFontSizeMenu.toggle() - } label: { - Image(systemName: "textformat.size") - } - .font(.title3) - .popover(isPresented: $showFontSizeMenu) { - VStack(alignment: .leading) { - Text(feedFlowStrings.readerModeFontSize) - - HStack { - Button { - fontSize -= 1.0 - self.reset.toggle() - vmStoreOwner.instance.updateFontSize(newFontSize: Int32(Int(fontSize))) - } label: { - Image(systemName: "minus") - } - - Slider( - value: $fontSize, - in: 12...40, - onEditingChanged: { isEditing in - if !isEditing { - self.reset.toggle() - vmStoreOwner.instance.updateFontSize(newFontSize: Int32(Int(fontSize))) - } - } - ) - - Button { - fontSize += 1.0 - self.reset.toggle() - vmStoreOwner.instance.updateFontSize(newFontSize: Int32(Int(fontSize))) - } label: { - Image(systemName: "plus") - } - } + .navigationBarTitleDisplayMode(.inline) + .task { + for await state in homeViewModel.navDrawerState { + self.navDrawerState = state + } + }.task { + for await state in vmStoreOwner.instance.readerFontSizeState { + self.fontSize = Double(truncating: state) + } + } + } + + @ViewBuilder + private var fontSizeMenu: some View { + Button { + showFontSizeMenu.toggle() + } label: { + Image(systemName: "textformat.size") + } + .font(.title3) + .popover(isPresented: $showFontSizeMenu) { + VStack(alignment: .leading) { + Text(feedFlowStrings.readerModeFontSize) + + HStack { + Button { + fontSize -= 1.0 + self.reset.toggle() + vmStoreOwner.instance.updateFontSize(newFontSize: Int32(Int(fontSize))) + } label: { + Image(systemName: "minus") + } + + Slider( + value: $fontSize, + in: 12...40, + onEditingChanged: { isEditing in + if !isEditing { + self.reset.toggle() + vmStoreOwner.instance.updateFontSize(newFontSize: Int32(Int(fontSize))) + } } - .frame(width: 250, height: 100) - .padding(.horizontal, Spacing.regular) - .presentationCompactAdaptation((.popover)) + ) + + Button { + fontSize += 1.0 + self.reset.toggle() + vmStoreOwner.instance.updateFontSize(newFontSize: Int32(Int(fontSize))) + } label: { + Image(systemName: "plus") + } } + } + .frame(width: 250, height: 100) + .padding(.horizontal, Spacing.regular) + .presentationCompactAdaptation((.popover)) } + } } diff --git a/iosApp/Source/App/Model/CommonViewRoute.swift b/iosApp/Source/App/Model/CommonViewRoute.swift index 9baf3f54..ad1779cb 100644 --- a/iosApp/Source/App/Model/CommonViewRoute.swift +++ b/iosApp/Source/App/Model/CommonViewRoute.swift @@ -6,11 +6,12 @@ // Copyright © 2024 FeedFlow. All rights reserved. // +import FeedFlowKit import Foundation enum CommonViewRoute: Hashable { - case readerMode(url: URL) - case search - case accounts - case dropboxSync + case readerMode(feedItem: FeedItem) + case search + case accounts + case dropboxSync } diff --git a/iosApp/Source/App/RegularView.swift b/iosApp/Source/App/RegularView.swift index 0e45ef38..267d7ff5 100644 --- a/iosApp/Source/App/RegularView.swift +++ b/iosApp/Source/App/RegularView.swift @@ -35,6 +35,7 @@ struct RegularView: View { @State private var fontSize = 16.0 @State private var isSliderMoving = false @State private var reset = false + @State private var isBookmarked = false @StateObject private var vmStoreOwner = VMStoreOwner( Deps.shared.getReaderModeViewModel()) @@ -99,44 +100,66 @@ struct RegularView: View { .navigationBarTitleDisplayMode(.inline) .navigationDestination(for: CommonViewRoute.self) { route in switch route { - case .readerMode(let url): - ReeeederView( - url: url, - options: ReeeederViewOptions( - theme: .init( - - additionalCSS: """ - #__reader_container { - font-size: \(fontSize)px - } - """ + case .readerMode(let feedItem): + Group { + ReeeederView( + url: URL(string: feedItem.url)!, + options: ReeeederViewOptions( + theme: .init( + + additionalCSS: """ + #__reader_container { + font-size: \(fontSize)px + } + """ + ), + onLinkClicked: { url in + if browserSelector.openInAppBrowser() { + browserToOpen = .inAppBrowser(url: url) + } else { + openURL(browserSelector.getUrlForDefaultBrowser(stringUrl: url.absoluteString)) + } + } ), - onLinkClicked: { url in - if browserSelector.openInAppBrowser() { - browserToOpen = .inAppBrowser(url: url) - } else { - openURL(browserSelector.getUrlForDefaultBrowser(stringUrl: url.absoluteString)) + toolbarContent: { + Button { + isBookmarked.toggle() + vmStoreOwner.instance.updateBookmarkStatus( + feedItemId: FeedItemId(id: feedItem.id), + bookmarked: isBookmarked + ) + } label: { + if isBookmarked { + Image(systemName: "bookmark.slash") + } else { + Image(systemName: "bookmark") + } } - } - ), - toolbarContent: { - Button { - if browserSelector.openInAppBrowser() { - browserToOpen = .inAppBrowser(url: url) - } else { - openURL(browserSelector.getUrlForDefaultBrowser(stringUrl: url.absoluteString)) + + ShareLink(item: URL(string: feedItem.url)!) { + Label("Share", systemImage: "square.and.arrow.up") } - } label: { - Image(systemName: "globe") - } - ShareLink(item: url) { - Label("Share", systemImage: "square.and.arrow.up") + Button { + if browserSelector.openInAppBrowser() { + browserToOpen = .inAppBrowser(url: URL(string: feedItem.url)!) + } else { + openURL( + browserSelector.getUrlForDefaultBrowser( + stringUrl: URL(string: feedItem.url)!.absoluteString)) + } + } label: { + Image(systemName: "globe") + } + + fontSizeMenu } - fontSizeMenu + ) + .onAppear { + isBookmarked = feedItem.isBookmarked } - ) - .id(reset) + .id(reset) + } case .search: SearchScreen() diff --git a/iosApp/Source/Home/Components/FeedListView.swift b/iosApp/Source/Home/Components/FeedListView.swift index cffa1509..06018df9 100644 --- a/iosApp/Source/Home/Components/FeedListView.swift +++ b/iosApp/Source/Home/Components/FeedListView.swift @@ -56,7 +56,7 @@ struct FeedListView: View { Button(action: { if browserSelector.openReaderMode() { self.appState.navigate( - route: CommonViewRoute.readerMode(url: URL(string: feedItem.url)!) + route: CommonViewRoute.readerMode(feedItem: feedItem) ) } else if browserSelector.openInAppBrowser() { browserToOpen = .inAppBrowser(url: URL(string: feedItem.url)!) @@ -68,7 +68,8 @@ struct FeedListView: View { id: feedItem.id, url: feedItem.url, title: feedItem.title, - openOnlyOnBrowser: false + openOnlyOnBrowser: false, + isBookmarked: feedItem.isBookmarked ) ) }, diff --git a/iosApp/Source/Search/SearchScreenContent.swift b/iosApp/Source/Search/SearchScreenContent.swift index de045f85..83c22793 100644 --- a/iosApp/Source/Search/SearchScreenContent.swift +++ b/iosApp/Source/Search/SearchScreenContent.swift @@ -49,7 +49,7 @@ struct SearchScreenContent: View { Button(action: { if browserSelector.openReaderMode() { self.appState.navigate( - route: CommonViewRoute.readerMode(url: URL(string: feedItem.url)!) + route: CommonViewRoute.readerMode(feedItem: feedItem) ) } else if browserSelector.openInAppBrowser() { browserToOpen = .inAppBrowser(url: URL(string: feedItem.url)!) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index c13feb60..22cffd67 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -135,6 +135,7 @@ kotlin { dependencies { implementation(libs.kotlinx.coroutines.swing) + implementation(libs.flexmark.html2md.converter) api(libs.sentry) } } diff --git a/shared/src/androidMain/kotlin/com/prof18/feedflow/shared/di/KoinAndroid.kt b/shared/src/androidMain/kotlin/com/prof18/feedflow/shared/di/KoinAndroid.kt index ee89f6e5..1b4fc387 100644 --- a/shared/src/androidMain/kotlin/com/prof18/feedflow/shared/di/KoinAndroid.kt +++ b/shared/src/androidMain/kotlin/com/prof18/feedflow/shared/di/KoinAndroid.kt @@ -14,6 +14,7 @@ import com.prof18.feedflow.shared.domain.feedsync.SyncWorkManager import com.prof18.feedflow.shared.domain.model.CurrentOS import com.prof18.feedflow.shared.domain.opml.OpmlFeedHandler import com.prof18.feedflow.shared.presentation.DropboxSyncViewModel +import com.prof18.feedflow.shared.presentation.ReaderModeViewModel import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings import kotlinx.coroutines.CoroutineDispatcher @@ -21,6 +22,7 @@ import kotlinx.coroutines.Dispatchers import org.koin.androidx.workmanager.dsl.workerOf import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.viewModel import org.koin.dsl.bind import org.koin.dsl.module @@ -89,4 +91,12 @@ internal actual fun getPlatformModule(appEnvironment: AppEnvironment): Module = factory { CurrentOS.Android } workerOf(::SyncWorkManager) + + viewModel { + ReaderModeViewModel( + readerModeExtractor = get(), + settingsRepository = get(), + feedRetrieverRepository = get(), + ) + } } diff --git a/androidApp/src/main/kotlin/com/prof18/feedflow/android/readermode/ReaderModeViewModel.android.kt b/shared/src/androidMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.android.kt similarity index 73% rename from androidApp/src/main/kotlin/com/prof18/feedflow/android/readermode/ReaderModeViewModel.android.kt rename to shared/src/androidMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.android.kt index 9689a9c2..ebaaec11 100644 --- a/androidApp/src/main/kotlin/com/prof18/feedflow/android/readermode/ReaderModeViewModel.android.kt +++ b/shared/src/androidMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.android.kt @@ -1,10 +1,12 @@ -package com.prof18.feedflow.android.readermode +package com.prof18.feedflow.shared.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.prof18.feedflow.core.model.FeedItemId import com.prof18.feedflow.core.model.FeedItemUrlInfo import com.prof18.feedflow.core.model.ReaderModeState import com.prof18.feedflow.shared.domain.ReaderModeExtractor +import com.prof18.feedflow.shared.domain.feed.retriever.FeedRetrieverRepository import com.prof18.feedflow.shared.domain.settings.SettingsRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,6 +16,7 @@ import kotlinx.coroutines.launch class ReaderModeViewModel internal constructor( private val readerModeExtractor: ReaderModeExtractor, private val settingsRepository: SettingsRepository, + private val feedRetrieverRepository: FeedRetrieverRepository, ) : ViewModel() { private val readerModeMutableState: MutableStateFlow = MutableStateFlow( @@ -33,7 +36,11 @@ class ReaderModeViewModel internal constructor( if (readerModeData != null) { readerModeMutableState.value = ReaderModeState.Success(readerModeData) } else { - readerModeMutableState.value = ReaderModeState.HtmlNotAvailable(urlInfo.url) + readerModeMutableState.value = ReaderModeState.HtmlNotAvailable( + url = urlInfo.url, + id = urlInfo.id, + isBookmarked = urlInfo.isBookmarked, + ) } } } @@ -42,4 +49,10 @@ class ReaderModeViewModel internal constructor( settingsRepository.setReaderModeFontSize(newFontSize) readerFontSizeMutableState.update { newFontSize } } + + fun updateBookmarkStatus(feedItemId: FeedItemId, bookmarked: Boolean) { + viewModelScope.launch { + feedRetrieverRepository.updateBookmarkStatus(feedItemId, bookmarked) + } + } } diff --git a/shared/src/commonJvmAndroidMain/kotlin/com/prof18/feedflow/shared/domain/ReaderModeExtractor.kt b/shared/src/commonJvmAndroidMain/kotlin/com/prof18/feedflow/shared/domain/ReaderModeExtractor.kt index b2a0018f..4e923b4e 100644 --- a/shared/src/commonJvmAndroidMain/kotlin/com/prof18/feedflow/shared/domain/ReaderModeExtractor.kt +++ b/shared/src/commonJvmAndroidMain/kotlin/com/prof18/feedflow/shared/domain/ReaderModeExtractor.kt @@ -1,5 +1,6 @@ package com.prof18.feedflow.shared.domain +import com.prof18.feedflow.core.model.FeedItemId import com.prof18.feedflow.core.model.FeedItemUrlInfo import com.prof18.feedflow.core.model.ReaderModeData import com.prof18.feedflow.core.utils.DispatcherProvider @@ -22,10 +23,12 @@ class ReaderModeExtractor internal constructor( val contentWithDocumentsCharsetOrUtf8 = article.contentWithDocumentsCharsetOrUtf8 ?: return@withContext null return@withContext ReaderModeData( + id = FeedItemId(urlInfo.id), title = title, content = contentWithDocumentsCharsetOrUtf8, url = urlInfo.url, fontSize = settingsRepository.getReaderModeFontSize(), + isBookmarked = urlInfo.isBookmarked, ) } } diff --git a/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/di/KoinIOS.kt b/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/di/KoinIOS.kt index 89675045..5dba463e 100644 --- a/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/di/KoinIOS.kt +++ b/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/di/KoinIOS.kt @@ -133,6 +133,7 @@ internal actual fun getPlatformModule(appEnvironment: AppEnvironment): Module = viewModel { ReaderModeViewModel( settingsRepository = get(), + feedRetrieverRepository = get(), ) } } diff --git a/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.ios.kt b/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.ios.kt index c54b0b3a..6489345e 100644 --- a/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.ios.kt +++ b/shared/src/iosMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.ios.kt @@ -1,13 +1,18 @@ package com.prof18.feedflow.shared.presentation import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.prof18.feedflow.core.model.FeedItemId +import com.prof18.feedflow.shared.domain.feed.retriever.FeedRetrieverRepository import com.prof18.feedflow.shared.domain.settings.SettingsRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch -class ReaderModeViewModel( +class ReaderModeViewModel internal constructor( private val settingsRepository: SettingsRepository, + private val feedRetrieverRepository: FeedRetrieverRepository, ) : ViewModel() { private val readerFontSizeMutableState: MutableStateFlow = MutableStateFlow( @@ -19,4 +24,10 @@ class ReaderModeViewModel( settingsRepository.setReaderModeFontSize(newFontSize) readerFontSizeMutableState.update { newFontSize } } + + fun updateBookmarkStatus(feedItemId: FeedItemId, bookmarked: Boolean) { + viewModelScope.launch { + feedRetrieverRepository.updateBookmarkStatus(feedItemId, bookmarked) + } + } } diff --git a/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/di/KoinDesktop.kt b/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/di/KoinDesktop.kt index 98037170..e42dfca4 100644 --- a/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/di/KoinDesktop.kt +++ b/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/di/KoinDesktop.kt @@ -16,8 +16,11 @@ import com.prof18.feedflow.shared.domain.model.CurrentOS import com.prof18.feedflow.shared.domain.opml.OpmlFeedHandler import com.prof18.feedflow.shared.logging.SentryLogWriter import com.prof18.feedflow.shared.presentation.DropboxSyncViewModel +import com.prof18.feedflow.shared.presentation.MarkdownToHtmlConverter +import com.prof18.feedflow.shared.presentation.ReaderModeViewModel import com.russhwolf.settings.PreferencesSettings import com.russhwolf.settings.Settings +import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import org.koin.core.KoinApplication @@ -113,4 +116,13 @@ internal actual fun getPlatformModule(appEnvironment: AppEnvironment): Module = DesktopOS.LINUX -> CurrentOS.Desktop.Linux } } + + single { + MarkdownToHtmlConverter( + converter = FlexmarkHtmlConverter.builder().build(), + dispatcherProvider = get(), + ) + } + + factoryOf(::ReaderModeViewModel) } diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/MarkdownToHtmlConverter.kt b/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/presentation/MarkdownToHtmlConverter.kt similarity index 90% rename from desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/MarkdownToHtmlConverter.kt rename to shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/presentation/MarkdownToHtmlConverter.kt index 0b92c204..6f121b57 100644 --- a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/MarkdownToHtmlConverter.kt +++ b/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/presentation/MarkdownToHtmlConverter.kt @@ -1,4 +1,4 @@ -package com.prof18.feedflow.desktop.reaadermode +package com.prof18.feedflow.shared.presentation import com.prof18.feedflow.core.utils.DispatcherProvider import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter diff --git a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/ReaderModeViewModel.desktop.kt b/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.desktop.kt similarity index 80% rename from desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/ReaderModeViewModel.desktop.kt rename to shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.desktop.kt index 17434719..1c1875d9 100644 --- a/desktopApp/src/jvmMain/kotlin/com/prof18/feedflow/desktop/reaadermode/ReaderModeViewModel.desktop.kt +++ b/shared/src/jvmMain/kotlin/com/prof18/feedflow/shared/presentation/ReaderModeViewModel.desktop.kt @@ -1,10 +1,12 @@ -package com.prof18.feedflow.desktop.reaadermode +package com.prof18.feedflow.shared.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.prof18.feedflow.core.model.FeedItemId import com.prof18.feedflow.core.model.FeedItemUrlInfo import com.prof18.feedflow.core.model.ReaderModeState import com.prof18.feedflow.shared.domain.ReaderModeExtractor +import com.prof18.feedflow.shared.domain.feed.retriever.FeedRetrieverRepository import com.prof18.feedflow.shared.domain.getReaderModeStyledHtml import com.prof18.feedflow.shared.domain.settings.SettingsRepository import kotlinx.coroutines.flow.MutableStateFlow @@ -16,6 +18,7 @@ class ReaderModeViewModel internal constructor( private val readerModeExtractor: ReaderModeExtractor, private val markdownToHtmlConverter: MarkdownToHtmlConverter, private val settingsRepository: SettingsRepository, + private val feedRetrieverRepository: FeedRetrieverRepository, ) : ViewModel() { private val readerModeMutableState: MutableStateFlow = MutableStateFlow( @@ -52,7 +55,11 @@ class ReaderModeViewModel internal constructor( ), ) } else { - readerModeMutableState.value = ReaderModeState.HtmlNotAvailable(urlInfo.url) + readerModeMutableState.value = ReaderModeState.HtmlNotAvailable( + url = urlInfo.url, + id = urlInfo.id, + isBookmarked = urlInfo.isBookmarked, + ) } } } @@ -61,4 +68,10 @@ class ReaderModeViewModel internal constructor( settingsRepository.setReaderModeFontSize(newFontSize) readerFontSizeMutableState.update { newFontSize } } + + fun updateBookmarkStatus(feedItemId: FeedItemId, bookmarked: Boolean) { + viewModelScope.launch { + feedRetrieverRepository.updateBookmarkStatus(feedItemId, bookmarked) + } + } } diff --git a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/home/components/FeedListView.kt b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/home/components/FeedListView.kt index abd26e05..d444938d 100644 --- a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/home/components/FeedListView.kt +++ b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/home/components/FeedListView.kt @@ -144,6 +144,7 @@ internal fun FeedItemView( id = feedItem.id, url = feedItem.url, title = feedItem.title, + isBookmarked = feedItem.isBookmarked, ), ) }, @@ -358,6 +359,7 @@ private fun OpenCommentsMenuItem( url = requireNotNull(feedItem.commentsUrl), title = feedItem.title, openOnlyOnBrowser = true, + isBookmarked = feedItem.isBookmarked, ), ) closeMenu() diff --git a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/readermode/ReaderMode.kt b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/readermode/ReaderMode.kt index 2311f68f..5fb91226 100644 --- a/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/readermode/ReaderMode.kt +++ b/sharedUI/src/commonMain/kotlin/com/prof18/feedflow/shared/ui/readermode/ReaderMode.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.BookmarkAdd +import androidx.compose.material.icons.filled.BookmarkRemove import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.TextFields @@ -28,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow +import com.prof18.feedflow.core.model.FeedItemId import com.prof18.feedflow.core.model.ReaderModeState import com.prof18.feedflow.core.utils.TestingTag import com.prof18.feedflow.shared.ui.style.Spacing @@ -43,6 +46,7 @@ fun ReaderModeContent( onShareClick: (String) -> Unit, modifier: Modifier = Modifier, onFontSizeChange: (Int) -> Unit, + onBookmarkClick: (FeedItemId, Boolean) -> Unit, snackbarHost: @Composable () -> Unit = {}, readerModeSuccessView: @Composable (PaddingValues, ReaderModeState.Success) -> Unit, ) { @@ -61,6 +65,7 @@ fun ReaderModeContent( openInBrowser = openInBrowser, onShareClick = onShareClick, onFontSizeChange = onFontSizeChange, + onBookmarkClick = onBookmarkClick, ) }, snackbarHost = snackbarHost, @@ -102,6 +107,7 @@ private fun ReaderModeToolbar( openInBrowser: (String) -> Unit, onShareClick: (String) -> Unit, onFontSizeChange: (Int) -> Unit, + onBookmarkClick: (FeedItemId, Boolean) -> Unit, ) { var showMenu by remember { mutableStateOf(false) } @@ -127,25 +133,50 @@ private fun ReaderModeToolbar( }, actions = { Row { + if (readerModeState is ReaderModeState.HtmlNotAvailable) { + var isBookmarked by remember { + mutableStateOf(readerModeState.isBookmarked) + } + BookmarkButton( + isBookmarked = isBookmarked, + onClick = { + isBookmarked = !isBookmarked + onBookmarkClick(FeedItemId(readerModeState.id), isBookmarked) + }, + ) + } + if (readerModeState is ReaderModeState.Success) { + var isBookmarked by remember { + mutableStateOf(readerModeState.readerModeData.isBookmarked) + } + + BookmarkButton( + isBookmarked = isBookmarked, + onClick = { + isBookmarked = !isBookmarked + onBookmarkClick(readerModeState.readerModeData.id, isBookmarked) + }, + ) + IconButton( onClick = { - openInBrowser(readerModeState.readerModeData.url) + onShareClick(readerModeState.readerModeData.url) }, ) { Icon( - imageVector = Icons.Default.Language, + imageVector = Icons.Default.Share, contentDescription = null, ) } IconButton( onClick = { - onShareClick(readerModeState.readerModeData.url) + openInBrowser(readerModeState.readerModeData.url) }, ) { Icon( - imageVector = Icons.Default.Share, + imageVector = Icons.Default.Language, contentDescription = null, ) } @@ -170,7 +201,7 @@ private fun ReaderModeToolbar( }, ) { Column( - modifier = Modifier.padding(Spacing.regular) + modifier = Modifier.padding(Spacing.regular), ) { Text( text = LocalFeedFlowStrings.current.readerModeFontSize, @@ -192,3 +223,22 @@ private fun ReaderModeToolbar( }, ) } + +@Composable +private fun BookmarkButton( + isBookmarked: Boolean, + onClick: () -> Unit, +) { + IconButton( + onClick = onClick, + ) { + Icon( + imageVector = if (isBookmarked) { + Icons.Default.BookmarkRemove + } else { + Icons.Default.BookmarkAdd + }, + contentDescription = null, + ) + } +}