Merge pull request #732 from omnivore-app/feature/ios-web-prefs
iOS web prefs
This commit is contained in:
@ -15,6 +15,10 @@ import WebKit
|
||||
|
||||
@Binding var increaseFontActionID: UUID?
|
||||
@Binding var decreaseFontActionID: UUID?
|
||||
@Binding var increaseMarginActionID: UUID?
|
||||
@Binding var decreaseMarginActionID: UUID?
|
||||
@Binding var increaseLineHeightActionID: UUID?
|
||||
@Binding var decreaseLineHeightActionID: UUID?
|
||||
@Binding var annotationSaveTransactionID: UUID?
|
||||
@Binding var showNavBarActionID: UUID?
|
||||
@Binding var shareActionID: UUID?
|
||||
@ -29,6 +33,16 @@ import WebKit
|
||||
return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize
|
||||
}
|
||||
|
||||
func lineHeight() -> Int {
|
||||
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebLineSpacing.rawValue)
|
||||
return storedSize <= 1 ? 150 : storedSize
|
||||
}
|
||||
|
||||
func margin() -> Int {
|
||||
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebMargin.rawValue)
|
||||
return storedSize <= 1 ? 360 : storedSize
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let webView = WebViewManager.shared()
|
||||
let contentController = WKUserContentController()
|
||||
@ -77,6 +91,26 @@ import WebKit
|
||||
(webView as? WebView)?.decreaseFontSize()
|
||||
}
|
||||
|
||||
if increaseMarginActionID != context.coordinator.previousIncreaseMarginActionID {
|
||||
context.coordinator.previousIncreaseMarginActionID = increaseMarginActionID
|
||||
(webView as? WebView)?.increaseMargin()
|
||||
}
|
||||
|
||||
if decreaseMarginActionID != context.coordinator.previousDecreaseMarginActionID {
|
||||
context.coordinator.previousDecreaseMarginActionID = decreaseMarginActionID
|
||||
(webView as? WebView)?.decreaseMargin()
|
||||
}
|
||||
|
||||
if increaseLineHeightActionID != context.coordinator.previousIncreaseLineHeightActionID {
|
||||
context.coordinator.previousIncreaseLineHeightActionID = increaseLineHeightActionID
|
||||
(webView as? WebView)?.increaseLineHeight()
|
||||
}
|
||||
|
||||
if decreaseLineHeightActionID != context.coordinator.previousDecreaseLineHeightActionID {
|
||||
context.coordinator.previousDecreaseLineHeightActionID = decreaseLineHeightActionID
|
||||
(webView as? WebView)?.decreaseLineHeight()
|
||||
}
|
||||
|
||||
if showNavBarActionID != context.coordinator.previousShowNavBarActionID {
|
||||
context.coordinator.previousShowNavBarActionID = showNavBarActionID
|
||||
context.coordinator.showNavBar()
|
||||
@ -116,7 +150,9 @@ import WebKit
|
||||
highlightsJSONString: highlightsJSONString,
|
||||
item: item,
|
||||
isDark: UITraitCollection.current.userInterfaceStyle == .dark,
|
||||
fontSize: fontSize()
|
||||
fontSize: fontSize(),
|
||||
lineHeight: lineHeight(),
|
||||
margin: margin()
|
||||
)
|
||||
.styledContent,
|
||||
baseURL: ViewsPackage.bundleURL
|
||||
|
||||
@ -8,7 +8,7 @@ import WebKit
|
||||
struct WebReaderContainerView: View {
|
||||
let item: LinkedItem
|
||||
|
||||
@State private var showFontSizePopover = false
|
||||
@State private var showPreferencesPopover = false
|
||||
@State private var showLabelsModal = false
|
||||
@State var showHighlightAnnotationModal = false
|
||||
@State var safariWebLink: SafariWebLink?
|
||||
@ -17,6 +17,10 @@ import WebKit
|
||||
@State private var progressViewOpacity = 0.0
|
||||
@State var increaseFontActionID: UUID?
|
||||
@State var decreaseFontActionID: UUID?
|
||||
@State var increaseMarginActionID: UUID?
|
||||
@State var decreaseMarginActionID: UUID?
|
||||
@State var increaseLineHeightActionID: UUID?
|
||||
@State var decreaseLineHeightActionID: UUID?
|
||||
@State var annotationSaveTransactionID: UUID?
|
||||
@State var showNavBarActionID: UUID?
|
||||
@State var shareActionID: UUID?
|
||||
@ -26,13 +30,6 @@ import WebKit
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
@StateObject var viewModel = WebReaderViewModel()
|
||||
|
||||
var fontAdjustmentPopoverView: some View {
|
||||
FontSizeAdjustmentPopoverView(
|
||||
increaseFontAction: { increaseFontActionID = UUID() },
|
||||
decreaseFontAction: { decreaseFontActionID = UUID() }
|
||||
)
|
||||
}
|
||||
|
||||
func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) {
|
||||
if let replyHandler = replyHandler {
|
||||
viewModel.webViewActionWithReplyHandler(
|
||||
@ -75,7 +72,7 @@ import WebKit
|
||||
.scaleEffect(navBarVisibilityRatio)
|
||||
Spacer()
|
||||
Button(
|
||||
action: { showFontSizePopover.toggle() },
|
||||
action: { showPreferencesPopover.toggle() },
|
||||
label: {
|
||||
Image(systemName: "textformat.size")
|
||||
.font(.appTitleTwo)
|
||||
@ -122,9 +119,6 @@ import WebKit
|
||||
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
|
||||
.opacity(navBarVisibilityRatio)
|
||||
.background(Color.systemBackground)
|
||||
.onTapGesture {
|
||||
showFontSizePopover = false
|
||||
}
|
||||
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
|
||||
Button("Remove Link", role: .destructive) {
|
||||
Snackbar.show(message: "Link removed")
|
||||
@ -153,13 +147,14 @@ import WebKit
|
||||
},
|
||||
webViewActionHandler: webViewActionHandler,
|
||||
navBarVisibilityRatioUpdater: {
|
||||
if $0 < 1 {
|
||||
showFontSizePopover = false
|
||||
}
|
||||
navBarVisibilityRatio = $0
|
||||
},
|
||||
increaseFontActionID: $increaseFontActionID,
|
||||
decreaseFontActionID: $decreaseFontActionID,
|
||||
increaseMarginActionID: $increaseMarginActionID,
|
||||
decreaseMarginActionID: $decreaseMarginActionID,
|
||||
increaseLineHeightActionID: $increaseLineHeightActionID,
|
||||
decreaseLineHeightActionID: $decreaseLineHeightActionID,
|
||||
annotationSaveTransactionID: $annotationSaveTransactionID,
|
||||
showNavBarActionID: $showNavBarActionID,
|
||||
shareActionID: $shareActionID,
|
||||
@ -200,33 +195,23 @@ import WebKit
|
||||
await viewModel.loadContent(dataService: dataService, itemID: item.unwrappedID)
|
||||
}
|
||||
}
|
||||
if showFontSizePopover {
|
||||
VStack {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.frame(height: LinkItemDetailView.navBarHeight)
|
||||
HStack {
|
||||
Spacer()
|
||||
fontAdjustmentPopoverView
|
||||
.background(Color.appButtonBackground)
|
||||
.cornerRadius(8)
|
||||
.padding(.trailing, 44)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.background(
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
showFontSizePopover = false
|
||||
}
|
||||
)
|
||||
}
|
||||
VStack(spacing: 0) {
|
||||
navBar
|
||||
Spacer()
|
||||
}
|
||||
}.onDisappear {
|
||||
}
|
||||
.formSheet(isPresented: $showPreferencesPopover) {
|
||||
WebPreferencesPopoverView(
|
||||
increaseFontAction: { increaseFontActionID = UUID() },
|
||||
decreaseFontAction: { decreaseFontActionID = UUID() },
|
||||
increaseMarginAction: { increaseMarginActionID = UUID() },
|
||||
decreaseMarginAction: { decreaseMarginActionID = UUID() },
|
||||
increaseLineHeightAction: { increaseLineHeightActionID = UUID() },
|
||||
decreaseLineHeightAction: { decreaseLineHeightActionID = UUID() },
|
||||
dismissAction: { showPreferencesPopover = false }
|
||||
)
|
||||
}
|
||||
.onDisappear {
|
||||
// Clear the shared webview content when exiting
|
||||
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import Utils
|
||||
|
||||
struct WebReaderContent {
|
||||
let textFontSize: Int
|
||||
let lineHeight: Int
|
||||
let margin: Int
|
||||
let htmlContent: String
|
||||
let highlightsJSONString: String
|
||||
let item: LinkedItem
|
||||
@ -14,9 +16,13 @@ struct WebReaderContent {
|
||||
highlightsJSONString: String,
|
||||
item: LinkedItem,
|
||||
isDark: Bool,
|
||||
fontSize: Int
|
||||
fontSize: Int,
|
||||
lineHeight: Int,
|
||||
margin: Int
|
||||
) {
|
||||
self.textFontSize = fontSize
|
||||
self.lineHeight = lineHeight
|
||||
self.margin = margin
|
||||
self.htmlContent = htmlContent
|
||||
self.highlightsJSONString = highlightsJSONString
|
||||
self.item = item
|
||||
@ -71,6 +77,8 @@ struct WebReaderContent {
|
||||
}
|
||||
|
||||
window.fontSize = \(textFontSize)
|
||||
window.margin = \(margin)
|
||||
window.lineHeight = \(lineHeight)
|
||||
window.localStorage.setItem("theme", "\(themeKey)")
|
||||
</script>
|
||||
<script src="bundle.js"></script>
|
||||
|
||||
@ -17,6 +17,10 @@ final class WebReaderCoordinator: NSObject {
|
||||
var lastSavedAnnotationID: UUID?
|
||||
var previousIncreaseFontActionID: UUID?
|
||||
var previousDecreaseFontActionID: UUID?
|
||||
var previousIncreaseMarginActionID: UUID?
|
||||
var previousDecreaseMarginActionID: UUID?
|
||||
var previousIncreaseLineHeightActionID: UUID?
|
||||
var previousDecreaseLineHeightActionID: UUID?
|
||||
var previousShowNavBarActionID: UUID?
|
||||
var previousShareActionID: UUID?
|
||||
var updateNavBarVisibilityRatio: (Double) -> Void = { _ in }
|
||||
|
||||
@ -2,6 +2,8 @@ import Foundation
|
||||
|
||||
public enum UserDefaultKey: String {
|
||||
case preferredWebFontSize
|
||||
case preferredWebLineSpacing
|
||||
case preferredWebMargin
|
||||
case userHasDeniedPushPrimer
|
||||
case firebasePushToken
|
||||
case homeFeedlayoutPreference
|
||||
|
||||
@ -34,6 +34,26 @@ public final class WebView: WKWebView {
|
||||
dispatchEvent("decreaseFontSize")
|
||||
}
|
||||
|
||||
public func increaseMargin() {
|
||||
print("increase margin")
|
||||
dispatchEvent("increaseMargin")
|
||||
}
|
||||
|
||||
public func decreaseMargin() {
|
||||
print("decrease margin")
|
||||
dispatchEvent("decreaseMargin")
|
||||
}
|
||||
|
||||
public func increaseLineHeight() {
|
||||
print("increase line height")
|
||||
dispatchEvent("increaseLineHeight")
|
||||
}
|
||||
|
||||
public func decreaseLineHeight() {
|
||||
print("decrease line height")
|
||||
dispatchEvent("decreaseLineHeight")
|
||||
}
|
||||
|
||||
public func shareOriginalItem() {
|
||||
dispatchEvent("share")
|
||||
}
|
||||
|
||||
@ -1,6 +1,143 @@
|
||||
import SwiftUI
|
||||
import Utils
|
||||
|
||||
public struct WebPreferencesPopoverView: View {
|
||||
let increaseFontAction: () -> Void
|
||||
let decreaseFontAction: () -> Void
|
||||
let increaseMarginAction: () -> Void
|
||||
let decreaseMarginAction: () -> Void
|
||||
let increaseLineHeightAction: () -> Void
|
||||
let decreaseLineHeightAction: () -> Void
|
||||
let dismissAction: () -> Void
|
||||
|
||||
static let preferredWebFontSizeKey = UserDefaultKey.preferredWebFontSize.rawValue
|
||||
#if os(macOS)
|
||||
@AppStorage(preferredWebFontSizeKey) var storedFontSize = Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16)
|
||||
#else
|
||||
@AppStorage(preferredWebFontSizeKey) var storedFontSize: Int = UITraitCollection.current.preferredWebFontSize
|
||||
#endif
|
||||
|
||||
@AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150
|
||||
@AppStorage(UserDefaultKey.preferredWebMargin.rawValue) var storedMargin = 360
|
||||
|
||||
public init(
|
||||
increaseFontAction: @escaping () -> Void,
|
||||
decreaseFontAction: @escaping () -> Void,
|
||||
increaseMarginAction: @escaping () -> Void,
|
||||
decreaseMarginAction: @escaping () -> Void,
|
||||
increaseLineHeightAction: @escaping () -> Void,
|
||||
decreaseLineHeightAction: @escaping () -> Void,
|
||||
dismissAction: @escaping () -> Void
|
||||
) {
|
||||
self.increaseFontAction = increaseFontAction
|
||||
self.decreaseFontAction = decreaseFontAction
|
||||
self.increaseMarginAction = increaseMarginAction
|
||||
self.decreaseMarginAction = decreaseMarginAction
|
||||
self.increaseLineHeightAction = increaseLineHeightAction
|
||||
self.decreaseLineHeightAction = decreaseLineHeightAction
|
||||
self.dismissAction = dismissAction
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
ZStack {
|
||||
Text("Preferences").font(.appTitleTwo)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(
|
||||
action: dismissAction,
|
||||
label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) }
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
LabelledStepper(
|
||||
labelText: "Font Size:",
|
||||
onIncrement: {
|
||||
storedFontSize = min(storedFontSize + 2, 28)
|
||||
increaseFontAction()
|
||||
},
|
||||
onDecrement: {
|
||||
storedFontSize = max(storedFontSize - 2, 10)
|
||||
decreaseFontAction()
|
||||
}
|
||||
)
|
||||
|
||||
if UIDevice.isIPad {
|
||||
LabelledStepper(
|
||||
labelText: "Margin:",
|
||||
onIncrement: {
|
||||
storedMargin = min(storedMargin + 45, 560)
|
||||
increaseMarginAction()
|
||||
},
|
||||
onDecrement: {
|
||||
storedMargin = max(storedMargin - 45, 200)
|
||||
decreaseMarginAction()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LabelledStepper(
|
||||
labelText: "Line Spacing:",
|
||||
onIncrement: {
|
||||
storedLineSpacing = min(storedLineSpacing + 25, 300)
|
||||
increaseLineHeightAction()
|
||||
},
|
||||
onDecrement: {
|
||||
storedLineSpacing = max(storedLineSpacing - 25, 100)
|
||||
decreaseLineHeightAction()
|
||||
}
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct LabelledStepper: View {
|
||||
let labelText: String
|
||||
let onIncrement: () -> Void
|
||||
let onDecrement: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text(labelText)
|
||||
Spacer()
|
||||
HStack(spacing: 0) {
|
||||
Button(
|
||||
action: onDecrement,
|
||||
label: {
|
||||
Image(systemName: "minus")
|
||||
#if os(iOS)
|
||||
.foregroundColor(.systemLabel)
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
)
|
||||
.frame(width: 55, height: 40, alignment: .center)
|
||||
Divider()
|
||||
.frame(height: 30)
|
||||
.background(Color.systemLabel)
|
||||
Button(
|
||||
action: onIncrement,
|
||||
label: {
|
||||
Image(systemName: "plus")
|
||||
#if os(iOS)
|
||||
.foregroundColor(.systemLabel)
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
)
|
||||
.frame(width: 55, height: 40, alignment: .center)
|
||||
}
|
||||
.background(Color.appButtonBackground)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct FontSizeAdjustmentPopoverView: View {
|
||||
let increaseFontAction: () -> Void
|
||||
let decreaseFontAction: () -> Void
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -7,10 +7,11 @@ import '@omnivore/web/styles/globals.css'
|
||||
import '@omnivore/web/styles/articleInnerStyling.css'
|
||||
|
||||
const mutation = async (name, input) => {
|
||||
const result = await window?.webkit?.messageHandlers.articleAction?.postMessage({
|
||||
actionID: name,
|
||||
...input
|
||||
})
|
||||
const result =
|
||||
await window?.webkit?.messageHandlers.articleAction?.postMessage({
|
||||
actionID: name,
|
||||
...input,
|
||||
})
|
||||
console.log('action result', result, result.result)
|
||||
return result.result
|
||||
}
|
||||
@ -40,13 +41,19 @@ const App = () => {
|
||||
highlightBarDisabled={true}
|
||||
highlightsBaseURL="https://example.com"
|
||||
fontSize={window.fontSize ?? 18}
|
||||
margin={0}
|
||||
margin={window.margin}
|
||||
lineHeight={window.lineHeight}
|
||||
articleMutations={{
|
||||
createHighlightMutation: (input) => mutation('createHighlight', input),
|
||||
deleteHighlightMutation: (highlightId) => mutation('deleteHighlight', { highlightId }),
|
||||
mergeHighlightMutation: (input) => mutation('mergeHighlight', input),
|
||||
updateHighlightMutation: (input) => mutation('updateHighlight', input),
|
||||
articleReadingProgressMutation: (input) => mutation('articleReadingProgress', input),
|
||||
createHighlightMutation: (input) =>
|
||||
mutation('createHighlight', input),
|
||||
deleteHighlightMutation: (highlightId) =>
|
||||
mutation('deleteHighlight', { highlightId }),
|
||||
mergeHighlightMutation: (input) =>
|
||||
mutation('mergeHighlight', input),
|
||||
updateHighlightMutation: (input) =>
|
||||
mutation('updateHighlight', input),
|
||||
articleReadingProgressMutation: (input) =>
|
||||
mutation('articleReadingProgress', input),
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
@ -40,6 +40,14 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
|
||||
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
|
||||
// iOS app embed can overide the original margin and line height
|
||||
const [marginOverride, setMarginOverride] = useState<number | null>(null)
|
||||
const [lineHeightOverride, setLineHeightOverride] = useState<number | null>(
|
||||
null
|
||||
)
|
||||
const [fontFamilyOverride, setFontFamilyOverride] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const highlightHref = useRef(
|
||||
window.location.hash ? window.location.hash.split('#')[1] : null
|
||||
)
|
||||
@ -74,8 +82,32 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
setHighlightReady(true)
|
||||
}, [props.article.highlights, setHighlightLocations])
|
||||
|
||||
// Listen for font size and color mode change events sent from host apps (ios, macos...)
|
||||
// Listen for preference change events sent from host apps (ios, macos...)
|
||||
useEffect(() => {
|
||||
const increaseLineHeight = () => {
|
||||
setLineHeightOverride(
|
||||
Math.min((lineHeightOverride ?? props.lineHeight ?? 150) + 25, 300)
|
||||
)
|
||||
}
|
||||
|
||||
const decreaseLineHeight = () => {
|
||||
setLineHeightOverride(
|
||||
Math.max((lineHeightOverride ?? props.lineHeight ?? 150) - 25, 100)
|
||||
)
|
||||
}
|
||||
|
||||
const increaseMargin = () => {
|
||||
setMarginOverride(
|
||||
Math.min((marginOverride ?? props.margin ?? 360) + 45, 560)
|
||||
)
|
||||
}
|
||||
|
||||
const decreaseMargin = () => {
|
||||
setMarginOverride(
|
||||
Math.max((marginOverride ?? props.margin ?? 360) - 45, 200)
|
||||
)
|
||||
}
|
||||
|
||||
const increaseFontSize = async () => {
|
||||
await updateFontSize(Math.min(fontSize + 2, 28))
|
||||
}
|
||||
@ -101,6 +133,10 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('increaseLineHeight', increaseLineHeight)
|
||||
document.addEventListener('decreaseLineHeight', decreaseLineHeight)
|
||||
document.addEventListener('increaseMargin', increaseMargin)
|
||||
document.addEventListener('decreaseMargin', decreaseMargin)
|
||||
document.addEventListener('increaseFontSize', increaseFontSize)
|
||||
document.addEventListener('decreaseFontSize', decreaseFontSize)
|
||||
document.addEventListener('switchToDarkMode', switchToDarkMode)
|
||||
@ -108,6 +144,10 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
document.addEventListener('share', share)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('increaseLineHeight', increaseLineHeight)
|
||||
document.removeEventListener('decreaseLineHeight', decreaseLineHeight)
|
||||
document.removeEventListener('increaseMargin', increaseMargin)
|
||||
document.removeEventListener('decreaseMargin', decreaseMargin)
|
||||
document.removeEventListener('increaseFontSize', increaseFontSize)
|
||||
document.removeEventListener('decreaseFontSize', decreaseFontSize)
|
||||
document.removeEventListener('switchToDarkMode', switchToDarkMode)
|
||||
@ -118,9 +158,9 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
|
||||
const styles = {
|
||||
fontSize,
|
||||
margin: props.margin ?? 360,
|
||||
lineHeight: props.lineHeight ?? 150,
|
||||
fontFamily: props.fontFamily ?? 'inter',
|
||||
margin: marginOverride ?? props.margin ?? 360,
|
||||
lineHeight: lineHeightOverride ?? props.lineHeight ?? 150,
|
||||
fontFamily: fontFamilyOverride ?? props.fontFamily ?? 'inter',
|
||||
readerFontColor: theme.colors.readerFont.toString(),
|
||||
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
|
||||
readerTableHeaderColor: theme.colors.readerTableHeader.toString(),
|
||||
|
||||
Reference in New Issue
Block a user