diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceLayoutScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceLayoutScreen.kt index 506c4c85cf..a6ae046884 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceLayoutScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceLayoutScreen.kt @@ -257,6 +257,29 @@ internal fun AppearanceLayoutScreen( ) }, ) + SegmentedListItem( + onClick = { + state.update( + AppearanceKeys.PostActionFixedWidth, + !timelineAppearance.postActionFixedWidth, + ) + }, + shapes = ListItemDefaults.item(), + content = { + Text(text = stringResource(id = R.string.settings_post_action_fixed_width)) + }, + supportingContent = { + Text(text = stringResource(id = R.string.settings_post_action_fixed_width_description)) + }, + trailingContent = { + Switch( + checked = timelineAppearance.postActionFixedWidth, + onCheckedChange = { + state.update(AppearanceKeys.PostActionFixedWidth, it) + }, + ) + }, + ) SegmentedListItem( onClick = toPostActionLayout, shapes = ListItemDefaults.last(), diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceMediaScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceMediaScreen.kt index ed36594395..43c1444535 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceMediaScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceMediaScreen.kt @@ -178,6 +178,31 @@ internal fun AppearanceMediaScreen(onBack: () -> Unit) { }, ) } + AnimatedVisibility(timelineAppearance.showMedia) { + SegmentedListItem( + onClick = { + state.update( + AppearanceKeys.LimitMediaGridToNine, + !timelineAppearance.limitMediaGridToNine, + ) + }, + shapes = ListItemDefaults.item(), + content = { + Text(text = stringResource(id = R.string.settings_appearance_limit_media_grid_to_nine)) + }, + supportingContent = { + Text(text = stringResource(id = R.string.settings_appearance_limit_media_grid_to_nine_description)) + }, + trailingContent = { + Switch( + checked = timelineAppearance.limitMediaGridToNine, + onCheckedChange = { + state.update(AppearanceKeys.LimitMediaGridToNine, it) + }, + ) + }, + ) + } AnimatedVisibility(timelineAppearance.showMedia) { SegmentedListItem( onClick = { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/TimelinePresentationEditor.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/TimelinePresentationEditor.kt index fffdf22164..81896c95fa 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/TimelinePresentationEditor.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/TimelinePresentationEditor.kt @@ -88,6 +88,7 @@ internal fun TimelinePresentationEditor( appearancePatch.contains(AppearanceKeys.TimelineDisplayMode) || appearancePatch.contains(AppearanceKeys.FullWidthPost) || appearancePatch.contains(AppearanceKeys.PostActionStyle) || + appearancePatch.contains(AppearanceKeys.PostActionFixedWidth) || appearancePatch.contains(AppearanceKeys.ShowNumbers) val displayOverridesEnabled = appearancePatch.contains(AppearanceKeys.AbsoluteTimestamp) || @@ -99,6 +100,7 @@ internal fun TimelinePresentationEditor( appearancePatch.contains(AppearanceKeys.ShowSensitiveContent) || appearancePatch.contains(AppearanceKeys.ExpandContentWarning) || appearancePatch.contains(AppearanceKeys.ExpandMediaSize) || + appearancePatch.contains(AppearanceKeys.LimitMediaGridToNine) || appearancePatch.contains(AppearanceKeys.VideoAutoplay) val themeOverridesEnabled = appearancePatch.contains(AppearanceKeys.AvatarShape) var showFilterDialog by remember { mutableStateOf(false) } @@ -217,11 +219,16 @@ internal fun TimelinePresentationEditor( AppearanceKeys.PostActionStyle, timelineAppearance.postActionStyle, ).set(AppearanceKeys.ShowNumbers, timelineAppearance.showNumbers) + .set( + AppearanceKeys.PostActionFixedWidth, + timelineAppearance.postActionFixedWidth, + ) } else { appearancePatch.clearAll( AppearanceKeys.TimelineDisplayMode, AppearanceKeys.FullWidthPost, AppearanceKeys.PostActionStyle, + AppearanceKeys.PostActionFixedWidth, AppearanceKeys.ShowNumbers, ) }, @@ -306,6 +313,20 @@ internal fun TimelinePresentationEditor( ), ) }, + shapes = ListItemDefaults.item(), + ) + SwitchSettingsItem( + title = stringResource(id = R.string.settings_post_action_fixed_width), + description = stringResource(id = R.string.settings_post_action_fixed_width_description), + checked = timelineAppearance.postActionFixedWidth, + onCheckedChange = { + onAppearancePatchChange( + appearancePatch.set( + AppearanceKeys.PostActionFixedWidth, + it, + ), + ) + }, shapes = ListItemDefaults.last(), ) } @@ -421,6 +442,9 @@ internal fun TimelinePresentationEditor( ).set( AppearanceKeys.ExpandMediaSize, timelineAppearance.expandMediaSize, + ).set( + AppearanceKeys.LimitMediaGridToNine, + timelineAppearance.limitMediaGridToNine, ).set(AppearanceKeys.VideoAutoplay, timelineAppearance.videoAutoplay) } else { appearancePatch.clearAll( @@ -428,6 +452,7 @@ internal fun TimelinePresentationEditor( AppearanceKeys.ShowSensitiveContent, AppearanceKeys.ExpandContentWarning, AppearanceKeys.ExpandMediaSize, + AppearanceKeys.LimitMediaGridToNine, AppearanceKeys.VideoAutoplay, ) }, @@ -480,6 +505,20 @@ internal fun TimelinePresentationEditor( }, shapes = ListItemDefaults.item(), ) + SwitchSettingsItem( + title = stringResource(id = R.string.settings_appearance_limit_media_grid_to_nine), + description = stringResource(id = R.string.settings_appearance_limit_media_grid_to_nine_description), + checked = timelineAppearance.limitMediaGridToNine, + onCheckedChange = { + onAppearancePatchChange( + appearancePatch.set( + AppearanceKeys.LimitMediaGridToNine, + it, + ), + ) + }, + shapes = ListItemDefaults.item(), + ) SwitchSettingsItem( title = stringResource(id = R.string.settings_appearance_expand_media), description = stringResource(id = R.string.settings_appearance_expand_media_description), diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 501cfb1fae..089a4ab81f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -259,6 +259,8 @@ 动态操作 自定义操作 选择哪些操作显示在按钮行、更多菜单或保持隐藏 + 按钮固定宽度 + 让带数字的操作按钮保持统一宽度 按钮行 更多菜单 隐藏 @@ -293,6 +295,8 @@ 总是显示内容警告隐藏的帖子内容 保留媒体比例 在时间轴中以原始宽高比显示媒体 + 媒体网格最多显示 9 项 + 每条时间轴动态最多显示 9 项媒体 视频自动播放 自动播放动态中的视频 仅限 Wi-Fi diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 665743a1bd..6223451a9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,6 +304,8 @@ Post actions Customize actions Choose which actions appear in the row, More menu, or stay hidden + Fixed button width + Keep action buttons with counts at a consistent width Button row More menu Hidden @@ -334,6 +336,8 @@ Always reveal post content hidden by content warnings Preserve media aspect ratio Show media at its original aspect ratio in timelines + Limit media grids to 9 items + Show at most 9 media items in each timeline post Info Comments Recommend diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AgentChatView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AgentChatView.swift index 9244de7c45..32a7029209 100644 --- a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AgentChatView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AgentChatView.swift @@ -560,12 +560,14 @@ private extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: true, expandMediaSize: false, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: .never, showLinkPreview: false, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: .hidden, postActionLayout: postActionLayout, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AppearanceSettingsSections.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AppearanceSettingsSections.swift index abf8efe7e7..a5a238b460 100644 --- a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AppearanceSettingsSections.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AppearanceSettingsSections.swift @@ -136,6 +136,14 @@ public struct AppearanceLayoutSettingsSection: View Text("appearance_show_numbers", bundle: FlareAppleUILocalization.bundle) Text("appearance_show_numbers_description", bundle: FlareAppleUILocalization.bundle) } + Toggle(isOn: Binding(get: { + appearance.postActionFixedWidth + }, set: { newValue in + presenter.state.updatePostActionFixedWidth(value: newValue) + })) { + Text("post_action_fixed_width", bundle: FlareAppleUILocalization.bundle) + Text("post_action_fixed_width_description", bundle: FlareAppleUILocalization.bundle) + } } postActionLayoutLink() } @@ -384,6 +392,14 @@ public struct AppearanceMediaSettingsSection: View { Text("appearance_expand_media_size", bundle: FlareAppleUILocalization.bundle) Text("appearance_expand_media_size_description", bundle: FlareAppleUILocalization.bundle) } + Toggle(isOn: Binding(get: { + appearance.limitMediaGridToNine + }, set: { newValue in + presenter.state.updateLimitMediaGridToNine(value: newValue) + })) { + Text("appearance_limit_media_grid_to_nine", bundle: FlareAppleUILocalization.bundle) + Text("appearance_limit_media_grid_to_nine_description", bundle: FlareAppleUILocalization.bundle) + } Toggle(isOn: Binding(get: { appearance.showSensitiveContent }, set: { newValue in diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/PostActionLayoutSupport.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/PostActionLayoutSupport.swift index fe04653508..f324eda27e 100644 --- a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/PostActionLayoutSupport.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/PostActionLayoutSupport.swift @@ -195,12 +195,14 @@ public extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: expandContentWarning, expandMediaSize: expandMediaSize, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: videoAutoplay, showLinkPreview: showLinkPreview, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: postActionStyle, postActionLayout: config, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusActionView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusActionView.swift index 81c2d852c6..f485364529 100644 --- a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusActionView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusActionView.swift @@ -19,6 +19,7 @@ public struct StatusActionsView: View { @Environment(\.timelineAppearance.postActionStyle) private var postActionStyle @Environment(\.timelineAppearance.showNumbers) private var showNumbers @Environment(\.timelineAppearance.postActionLayout) private var postActionLayout + @Environment(\.timelineAppearance.postActionFixedWidth) private var postActionFixedWidth @Environment(\.openURL) private var openURL #if os(macOS) @ScaledMetric(relativeTo: .callout) private var fontSize = 16 @@ -51,7 +52,7 @@ public struct StatusActionsView: View { ) } } else { - HStack { + HStack(spacing: postActionStyle == .stretch ? nil : 4) { ForEach(0.. some View { + if isEnabled { + ZStack(alignment: .leading) { + StatusActionFixedWidthPlaceholder(icon: icon, fontSize: fontSize) + .statusActionContentPadding(isExpanded: true, fontSize: fontSize) + .hidden() + self + } + } else { + self + } + } + @ViewBuilder func macOSStatusActionHoverStyle(isEnabled: Bool) -> some View { #if os(macOS) @@ -414,6 +450,20 @@ public struct StatusActionIcon: View { } } +private struct StatusActionFixedWidthPlaceholder: View { + let icon: UiIcon? + let fontSize: CGFloat + + var body: some View { + Label { + Text("") + .frame(minWidth: fontSize * 2.5) + } icon: { + StatusActionIcon(icon: icon) + } + } +} + private func castActionMenus(_ value: Any) -> [ActionMenu] { if let actions = value as? [ActionMenu] { return actions @@ -857,12 +907,14 @@ private extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: true, expandMediaSize: expandMediaSize, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: .never, showLinkPreview: showLinkPreview, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: postActionStyle, postActionLayout: postActionLayout, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaView.swift index 151fe6bb67..06476a24bc 100644 --- a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaView.swift @@ -12,6 +12,7 @@ struct StatusMediaView: View { let onMediaClicked: (any UiMedia, Int) -> Void let cornerRadius: CGFloat @Environment(\.timelineAppearance.expandMediaSize) private var expandMediaSize + @Environment(\.timelineAppearance.limitMediaGridToNine) private var limitMediaGridToNine @Environment(\.timelineMediaActionHandler) private var timelineMediaActionHandler @State private var isBlur: Bool // @State private var selectedIndex: Int? = nil @@ -32,7 +33,7 @@ struct StatusMediaView: View { } var body: some View { - let visibleData = Array(data.prefix(statusMediaMaxVisibleMediaCount)) + let visibleData = limitMediaGridToNine ? Array(data.prefix(statusMediaMaxVisibleMediaCount)) : data let overflowCount = data.count - visibleData.count AdaptiveGrid( singleFollowsImageAspect: expandMediaSize, diff --git a/appleApp/Shared/FlareAppleResource/Resources/Localizable.xcstrings b/appleApp/Shared/FlareAppleResource/Resources/Localizable.xcstrings index d3dfb23438..a97f5ccbf9 100644 --- a/appleApp/Shared/FlareAppleResource/Resources/Localizable.xcstrings +++ b/appleApp/Shared/FlareAppleResource/Resources/Localizable.xcstrings @@ -14178,6 +14178,50 @@ } } }, + "appearance_limit_media_grid_to_nine" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limit media grids to 9 items" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "媒体网格最多显示 9 项" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "媒體網格最多顯示 9 項" + } + } + } + }, + "appearance_limit_media_grid_to_nine_description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show at most 9 media items in each timeline post" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每条时间轴动态最多显示 9 项媒体" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "每則時間軸貼文最多顯示 9 項媒體" + } + } + } + }, "appearance_media_group_subtitle" : { "localizations" : { "af" : { @@ -117393,6 +117437,62 @@ } } }, + "post_action_fixed_width" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixed button width" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボタンの幅を固定" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "按钮固定宽度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按鈕固定寬度" + } + } + } + }, + "post_action_fixed_width_description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keep action buttons with counts at a consistent width" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カウント付きアクションボタンの幅を揃えます" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "让带数字的操作按钮保持统一宽度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "讓帶數字的操作按鈕保持一致寬度" + } + } + } + }, "post_action_layout_customize_description" : { "localizations" : { "af" : { @@ -159460,4 +159560,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/appleApp/ios/UI/Component/Status/StatusShareSheet.swift b/appleApp/ios/UI/Component/Status/StatusShareSheet.swift index 226a249b48..f62c608e2f 100644 --- a/appleApp/ios/UI/Component/Status/StatusShareSheet.swift +++ b/appleApp/ios/UI/Component/Status/StatusShareSheet.swift @@ -189,12 +189,14 @@ private extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: true, expandMediaSize: expandMediaSize, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: .never, showLinkPreview: showLinkPreview, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: postActionStyle, postActionLayout: postActionLayout, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/appleApp/ios/UI/Component/Status/StatusUIKitActions.swift b/appleApp/ios/UI/Component/Status/StatusUIKitActions.swift index f9b71f8adb..145f995d5e 100644 --- a/appleApp/ios/UI/Component/Status/StatusUIKitActions.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitActions.swift @@ -18,6 +18,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP private var allowSpacer: Bool = true private var postActionStyle: PostActionStyle = .leftAligned private var postActionLayout: PostActionLayoutConfig = PostActionLayoutConfig.companion.Default + private var postActionFixedWidth: Bool = true private var showNumbers: Bool = true private var fontSize: CGFloat = 13 private var textStyle: UIFont.TextStyle = .footnote @@ -43,6 +44,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP allowSpacer: Bool = true, postActionStyle: PostActionStyle, postActionLayout: PostActionLayoutConfig = PostActionLayoutConfig.companion.Default, + postActionFixedWidth: Bool = true, applyPostActionLayout: Bool = true, showNumbers: Bool, isDetail: Bool @@ -54,6 +56,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP self.allowSpacer = allowSpacer self.postActionStyle = postActionStyle self.postActionLayout = postActionLayout + self.postActionFixedWidth = postActionFixedWidth self.showNumbers = showNumbers self.fontSize = UIFontMetrics(forTextStyle: .footnote).scaledValue(for: 13) self.textStyle = isDetail ? .body : .footnote @@ -65,7 +68,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP } private var actionIconSize: CGFloat { - max(actionFont.pointSize + 1, fontSize * 1.2) + actionFont.pointSize } private func rebuild() { @@ -92,7 +95,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP if postActionStyle == .stretch, allowSpacer { isStretch = true for (index, item) in data.enumerated() { - if let v = makeActionView(for: item, isFixedWidth: index != data.count - 1) { + if let v = makeActionView(for: item, isFixedWidth: postActionFixedWidth && index != data.count - 1) { desired.append(v) } } @@ -111,7 +114,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP spacerIndex = desired.count } - if let v = makeActionView(for: item, isFixedWidth: index != data.count - 1) { + if let v = makeActionView(for: item, isFixedWidth: postActionFixedWidth && index != data.count - 1) { desired.append(v) } } @@ -150,7 +153,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP } // Compute total content width - let spacing: CGFloat = 8 + let spacing: CGFloat = 4 var totalContentWidth: CGFloat = 0 var childSizes: [CGSize] = [] for child in managedChildren { @@ -286,7 +289,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP let title: String? if useText, let text = item.text?.resolvedString { title = text - } else if showNumbers, let count = item.count?.humanized { + } else if showNumbers, let count = item.count?.humanized, !count.isEmpty { title = count } else { title = nil @@ -302,8 +305,9 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP tintColor: item.color?.uiColor ?? .secondaryLabel, font: actionFont, iconSize: actionIconSize, - minimumTextWidth: isFixedWidth && title != nil ? fontSize * 2.5 : nil, - minimumIconOnlySize: nil + minimumTextWidth: isFixedWidth && showNumbers && item.count != nil ? fontSize * 2.5 : nil, + minimumIconOnlySize: nil, + usesExpandedHitArea: !useText ) { [weak self] in guard let self = self else { return } let gen = UIImpactFeedbackGenerator(style: .medium) @@ -315,7 +319,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP private func makeGroupButton(group: ActionMenu.Group, isFixedWidth: Bool) -> UIView { let title: String? - if let text = group.displayItem.count?.humanized, showNumbers { + if let text = group.displayItem.count?.humanized, showNumbers, !text.isEmpty { title = text } else { title = nil @@ -331,8 +335,9 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP tintColor: group.displayItem.color?.uiColor ?? .secondaryLabel, font: actionFont, iconSize: actionIconSize, - minimumTextWidth: isFixedWidth && title != nil ? fontSize * 2.5 : nil, - minimumIconOnlySize: title == nil ? actionIconSize : nil + minimumTextWidth: isFixedWidth && showNumbers && group.displayItem.count != nil ? fontSize * 2.5 : nil, + minimumIconOnlySize: title == nil ? fontSize + 2 : nil, + usesExpandedHitArea: !useText ) control.showsMenuAsPrimaryAction = true control.menu = UIMenu(children: [ @@ -506,6 +511,7 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin private var minimumTextWidth: CGFloat? private var iconSize: CGFloat = 0 private var currentSpacing: CGFloat = 0 + private var horizontalInset: CGFloat = 0 private var onTap: (() -> Void)? override init(frame: CGRect) { @@ -520,7 +526,8 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin font: UIFont, iconSize: CGFloat, minimumTextWidth: CGFloat?, - minimumIconOnlySize: CGFloat? + minimumIconOnlySize: CGFloat?, + usesExpandedHitArea: Bool ) { self.init(frame: .zero) configure( @@ -531,6 +538,7 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin iconSize: iconSize, minimumTextWidth: minimumTextWidth, minimumIconOnlySize: minimumIconOnlySize, + usesExpandedHitArea: usesExpandedHitArea, onTap: nil ) } @@ -565,6 +573,7 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin iconSize: CGFloat, minimumTextWidth: CGFloat?, minimumIconOnlySize: CGFloat?, + usesExpandedHitArea: Bool, onTap: (() -> Void)? = nil ) { self.minimumIconOnlySize = minimumIconOnlySize @@ -572,7 +581,8 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin self.iconSize = iconSize self.onTap = onTap self.tintColor = tintColor - self.currentSpacing = title == nil ? 0 : 4 + self.currentSpacing = title != nil || minimumTextWidth != nil ? 4 : 0 + self.horizontalInset = usesExpandedHitArea ? 6 : 0 menu = nil showsMenuAsPrimaryAction = false @@ -599,6 +609,7 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin label.text = nil minimumTextWidth = nil minimumIconOnlySize = nil + horizontalInset = 0 } @objc private func onTapped() { @@ -608,15 +619,16 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin override func layoutSubviews() { super.layoutSubviews() let size = actionContentSize() - var x: CGFloat = 0 + let iconLayoutSize = resolvedIconLayoutSize() + var x = horizontalInset if !iconView.isHidden { iconView.frame = CGRect( x: x, - y: (size.height - iconSize) / 2, - width: iconSize, - height: iconSize + y: (size.height - iconLayoutSize.height) / 2, + width: iconLayoutSize.width, + height: iconLayoutSize.height ) - x += iconSize + currentSpacing + x += iconLayoutSize.width + currentSpacing } if !label.isHidden { let labelSize = label.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: size.height)) @@ -655,25 +667,40 @@ private final class ActionItemControl: UIButton, ManualLayoutMeasurable, Timelin } private func actionContentSize() -> CGSize { - let minimum = minimumIconOnlySize ?? 0 + let minimumIconOnlySize = minimumIconOnlySize ?? 0 var w: CGFloat = 0 var h: CGFloat = 0 if !iconView.isHidden { - w += iconSize - h = max(h, iconSize) + let iconLayoutSize = resolvedIconLayoutSize() + w += iconLayoutSize.width + h = max(h, iconLayoutSize.height) } if !label.isHidden { let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)) let labelW = max(labelSize.width, minimumTextWidth ?? 0) w += currentSpacing + labelW h = max(h, labelSize.height) + } else if let minimumTextWidth { + w += currentSpacing + minimumTextWidth } return CGSize( - width: ceil(max(w, minimum)), - height: ceil(max(h, minimum)) + width: ceil(max(w, minimumIconOnlySize) + horizontalInset * 2), + height: ceil(max(h, minimumIconOnlySize)) ) } + private func resolvedIconLayoutSize() -> CGSize { + let intrinsicSize = iconView.intrinsicContentSize + guard intrinsicSize.width > 0, + intrinsicSize.height > 0, + intrinsicSize.width.isFinite, + intrinsicSize.height.isFinite + else { + return CGSize(width: iconSize, height: iconSize) + } + return intrinsicSize + } + override var isHighlighted: Bool { didSet { alpha = isHighlighted ? 0.55 : 1 } } diff --git a/appleApp/ios/UI/Component/Status/StatusUIKitMedia.swift b/appleApp/ios/UI/Component/Status/StatusUIKitMedia.swift index e36d16567c..7fb5e65e81 100644 --- a/appleApp/ios/UI/Component/Status/StatusUIKitMedia.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitMedia.swift @@ -297,6 +297,7 @@ final class StatusMediaUIView: UIView, TimelineHeightProviding { private var cornerRadius: CGFloat = 16 private var isBlurred: Bool = false private var singleFollowsImageAspect: Bool = true + private var limitMediaGridToNine: Bool = true private var toggleButtonPositionConstraints: [NSLayoutConstraint] = [] private var aspectConstraint: NSLayoutConstraint? private var lastLayoutWidth: CGFloat = 0 @@ -309,17 +310,20 @@ final class StatusMediaUIView: UIView, TimelineHeightProviding { let sensitive: Bool let cornerRadius: CGFloat let singleFollowsImageAspect: Bool + let limitMediaGridToNine: Bool init( data: [UiMedia], sensitive: Bool, cornerRadius: CGFloat, - singleFollowsImageAspect: Bool + singleFollowsImageAspect: Bool, + limitMediaGridToNine: Bool ) { items = data.map(MediaItemSignature.init) self.sensitive = sensitive self.cornerRadius = cornerRadius self.singleFollowsImageAspect = singleFollowsImageAspect + self.limitMediaGridToNine = limitMediaGridToNine } } @@ -397,12 +401,19 @@ final class StatusMediaUIView: UIView, TimelineHeightProviding { return ceil(gridHeight(for: width)) } - func configure(data: [UiMedia], sensitive: Bool, cornerRadius: CGFloat, singleFollowsImageAspect: Bool) { + func configure( + data: [UiMedia], + sensitive: Bool, + cornerRadius: CGFloat, + singleFollowsImageAspect: Bool, + limitMediaGridToNine: Bool + ) { let signature = ConfigureSignature( data: data, sensitive: sensitive, cornerRadius: cornerRadius, - singleFollowsImageAspect: singleFollowsImageAspect + singleFollowsImageAspect: singleFollowsImageAspect, + limitMediaGridToNine: limitMediaGridToNine ) guard lastConfigureSignature != signature else { return } let shouldResetBlur = @@ -413,6 +424,7 @@ final class StatusMediaUIView: UIView, TimelineHeightProviding { self.sensitive = sensitive self.cornerRadius = cornerRadius self.singleFollowsImageAspect = singleFollowsImageAspect + self.limitMediaGridToNine = limitMediaGridToNine if shouldResetBlur { self.isBlurred = sensitive } @@ -643,7 +655,7 @@ final class StatusMediaUIView: UIView, TimelineHeightProviding { } private var visibleItemCount: Int { - min(items.count, Self.maxVisibleMediaCount) + limitMediaGridToNine ? min(items.count, Self.maxVisibleMediaCount) : items.count } private var overflowCount: Int { @@ -920,6 +932,7 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { private var appearanceShowMedia: Bool = true private var appearanceShowSensitive: Bool = false private var appearanceExpandMediaSize: Bool = true + private var appearanceLimitMediaGridToNine: Bool = true private var showButtonConstraints: [NSLayoutConstraint] = [] private var lastConfigureSignature: ConfigureSignature? @@ -930,6 +943,7 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { let appearanceShowMedia: Bool let appearanceShowSensitive: Bool let appearanceExpandMediaSize: Bool + let appearanceLimitMediaGridToNine: Bool init( data: [UiMedia], @@ -937,7 +951,8 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { cornerRadius: CGFloat, appearanceShowMedia: Bool, appearanceShowSensitive: Bool, - appearanceExpandMediaSize: Bool + appearanceExpandMediaSize: Bool, + appearanceLimitMediaGridToNine: Bool ) { items = data.map(MediaItemSignature.init) self.sensitive = sensitive @@ -945,6 +960,7 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { self.appearanceShowMedia = appearanceShowMedia self.appearanceShowSensitive = appearanceShowSensitive self.appearanceExpandMediaSize = appearanceExpandMediaSize + self.appearanceLimitMediaGridToNine = appearanceLimitMediaGridToNine } } @@ -1024,7 +1040,8 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { cornerRadius: CGFloat, appearanceShowMedia: Bool, appearanceShowSensitive: Bool, - appearanceExpandMediaSize: Bool + appearanceExpandMediaSize: Bool, + appearanceLimitMediaGridToNine: Bool ) { let signature = ConfigureSignature( data: data, @@ -1032,7 +1049,8 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { cornerRadius: cornerRadius, appearanceShowMedia: appearanceShowMedia, appearanceShowSensitive: appearanceShowSensitive, - appearanceExpandMediaSize: appearanceExpandMediaSize + appearanceExpandMediaSize: appearanceExpandMediaSize, + appearanceLimitMediaGridToNine: appearanceLimitMediaGridToNine ) guard lastConfigureSignature != signature else { return } let shouldResetExpanded = @@ -1045,6 +1063,7 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { self.appearanceShowMedia = appearanceShowMedia self.appearanceShowSensitive = appearanceShowSensitive self.appearanceExpandMediaSize = appearanceExpandMediaSize + self.appearanceLimitMediaGridToNine = appearanceLimitMediaGridToNine if shouldResetExpanded { self.expanded = false } @@ -1061,7 +1080,8 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { data: [], sensitive: false, cornerRadius: cornerRadius, - singleFollowsImageAspect: appearanceExpandMediaSize + singleFollowsImageAspect: appearanceExpandMediaSize, + limitMediaGridToNine: appearanceLimitMediaGridToNine ) grid.performDeferredPoolCleanup() grid.isHidden = true @@ -1092,7 +1112,8 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { data: items, sensitive: !appearanceShowSensitive && sensitive, cornerRadius: cornerRadius, - singleFollowsImageAspect: appearanceExpandMediaSize + singleFollowsImageAspect: appearanceExpandMediaSize, + limitMediaGridToNine: appearanceLimitMediaGridToNine ) } else { grid.isHidden = true @@ -1102,7 +1123,8 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { data: [], sensitive: false, cornerRadius: cornerRadius, - singleFollowsImageAspect: appearanceExpandMediaSize + singleFollowsImageAspect: appearanceExpandMediaSize, + limitMediaGridToNine: appearanceLimitMediaGridToNine ) } setNeedsLayout() diff --git a/appleApp/ios/UI/Component/Status/StatusUIKitView.swift b/appleApp/ios/UI/Component/Status/StatusUIKitView.swift index 07007d959a..1376b07e2f 100644 --- a/appleApp/ios/UI/Component/Status/StatusUIKitView.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitView.swift @@ -792,7 +792,8 @@ final class StatusUIKitView: UIView, UIGestureRecognizerDelegate, ManualLayoutMe cornerRadius: corner, appearanceShowMedia: appearance.showMedia, appearanceShowSensitive: appearance.showSensitiveContent, - appearanceExpandMediaSize: appearance.expandMediaSize + appearanceExpandMediaSize: appearance.expandMediaSize, + appearanceLimitMediaGridToNine: appearance.limitMediaGridToNine ) mediaView.onMediaClicked = { [weak self] media, index in guard let self else { return } @@ -892,6 +893,7 @@ final class StatusUIKitView: UIView, UIGestureRecognizerDelegate, ManualLayoutMe allowSpacer: true, postActionStyle: appearance.postActionStyle, postActionLayout: appearance.postActionLayout, + postActionFixedWidth: appearance.postActionFixedWidth, showNumbers: appearance.showNumbers, isDetail: isDetail ) diff --git a/appleApp/ios/UI/Component/TimelinePresentationEditor.swift b/appleApp/ios/UI/Component/TimelinePresentationEditor.swift index 5cb8543b40..0d8e79dd0c 100644 --- a/appleApp/ios/UI/Component/TimelinePresentationEditor.swift +++ b/appleApp/ios/UI/Component/TimelinePresentationEditor.swift @@ -387,6 +387,17 @@ private struct LayoutAppearanceOverrideGroup: View { Text("appearance_show_numbers") Text("appearance_show_numbers_description") } + Toggle(isOn: Binding(get: { + timelineAppearance.postActionFixedWidth + }, set: { value in + appearancePatch = TimelinePresentationAppearancePatchHelper.shared.setPostActionFixedWidth( + patch: appearancePatch, + value: value + ) + })) { + Text("post_action_fixed_width") + Text("post_action_fixed_width_description") + } } } } @@ -513,6 +524,17 @@ private struct MediaAppearanceOverrideGroup: View { Text("appearance_expand_media_size") Text("appearance_expand_media_size_description") } + Toggle(isOn: Binding(get: { + timelineAppearance.limitMediaGridToNine + }, set: { value in + appearancePatch = TimelinePresentationAppearancePatchHelper.shared.setLimitMediaGridToNine( + patch: appearancePatch, + value: value + ) + })) { + Text("appearance_limit_media_grid_to_nine") + Text("appearance_limit_media_grid_to_nine_description") + } Toggle(isOn: Binding(get: { timelineAppearance.showSensitiveContent }, set: { value in diff --git a/appleApp/ios/UI/Component/UIKitAppearance.swift b/appleApp/ios/UI/Component/UIKitAppearance.swift index e2658f4305..22d1220eb5 100644 --- a/appleApp/ios/UI/Component/UIKitAppearance.swift +++ b/appleApp/ios/UI/Component/UIKitAppearance.swift @@ -33,6 +33,7 @@ struct StatusUIKitAppearance: Equatable { let postActionStyleID: String let postActionLayout: PostActionLayoutConfig let postActionLayoutID: String + let postActionFixedWidth: Bool let showNumbers: Bool let showMedia: Bool let showSensitiveContent: Bool @@ -40,6 +41,7 @@ struct StatusUIKitAppearance: Equatable { let showLinkPreview: Bool let compatLinkPreview: Bool let expandMediaSize: Bool + let limitMediaGridToNine: Bool let aiAgentEnabled: Bool init(timeline: TimelineAppearance, fontSizeDiff: Float = 0) { @@ -54,6 +56,7 @@ struct StatusUIKitAppearance: Equatable { postActionStyleID = timeline.postActionStyle.name postActionLayout = timeline.postActionLayout postActionLayoutID = PostActionLayoutHelpers.shared.signature(config: timeline.postActionLayout) + postActionFixedWidth = timeline.postActionFixedWidth showNumbers = timeline.showNumbers showMedia = timeline.showMedia showSensitiveContent = timeline.showSensitiveContent @@ -61,6 +64,7 @@ struct StatusUIKitAppearance: Equatable { showLinkPreview = timeline.showLinkPreview compatLinkPreview = timeline.compatLinkPreview expandMediaSize = timeline.expandMediaSize + limitMediaGridToNine = timeline.limitMediaGridToNine aiAgentEnabled = timeline.aiConfig.agent } @@ -72,6 +76,7 @@ struct StatusUIKitAppearance: Equatable { lhs.absoluteTimestamp == rhs.absoluteTimestamp && lhs.postActionStyleID == rhs.postActionStyleID && lhs.postActionLayoutID == rhs.postActionLayoutID && + lhs.postActionFixedWidth == rhs.postActionFixedWidth && lhs.showNumbers == rhs.showNumbers && lhs.showMedia == rhs.showMedia && lhs.showSensitiveContent == rhs.showSensitiveContent && @@ -79,6 +84,7 @@ struct StatusUIKitAppearance: Equatable { lhs.showLinkPreview == rhs.showLinkPreview && lhs.compatLinkPreview == rhs.compatLinkPreview && lhs.expandMediaSize == rhs.expandMediaSize && + lhs.limitMediaGridToNine == rhs.limitMediaGridToNine && lhs.aiAgentEnabled == rhs.aiAgentEnabled } } diff --git a/appleApp/ios/UI/FlareTheme.swift b/appleApp/ios/UI/FlareTheme.swift index 21585be900..5e9b4efae2 100644 --- a/appleApp/ios/UI/FlareTheme.swift +++ b/appleApp/ios/UI/FlareTheme.swift @@ -65,12 +65,14 @@ private extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: expandContentWarning, expandMediaSize: expandMediaSize, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: videoAutoplay, showLinkPreview: showLinkPreview, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: postActionStyle, postActionLayout: postActionLayout, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/appleApp/ios/UI/Screen/GalleryDetailScreen.swift b/appleApp/ios/UI/Screen/GalleryDetailScreen.swift index fe42c02340..1932225e4b 100644 --- a/appleApp/ios/UI/Screen/GalleryDetailScreen.swift +++ b/appleApp/ios/UI/Screen/GalleryDetailScreen.swift @@ -777,12 +777,14 @@ private extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: expandContentWarning, expandMediaSize: expandMediaSize, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: videoAutoplay, showLinkPreview: showLinkPreview, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: postActionStyle, postActionLayout: postActionLayout, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/appleApp/ios/UI/Screen/PostActionLayoutScreen.swift b/appleApp/ios/UI/Screen/PostActionLayoutScreen.swift index c8baecbd68..d91f533b0a 100644 --- a/appleApp/ios/UI/Screen/PostActionLayoutScreen.swift +++ b/appleApp/ios/UI/Screen/PostActionLayoutScreen.swift @@ -228,12 +228,14 @@ private extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: expandContentWarning, expandMediaSize: expandMediaSize, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: videoAutoplay, showLinkPreview: showLinkPreview, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: postActionStyle, postActionLayout: config, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/appleApp/macos/App/MacSidebarAppearanceOverrideGroups.swift b/appleApp/macos/App/MacSidebarAppearanceOverrideGroups.swift index c57f25a9c9..3e248eea7d 100644 --- a/appleApp/macos/App/MacSidebarAppearanceOverrideGroups.swift +++ b/appleApp/macos/App/MacSidebarAppearanceOverrideGroups.swift @@ -79,6 +79,17 @@ struct MacSidebarLayoutAppearanceOverrideGroup: View { })) { Text("appearance_show_numbers") } + + Toggle(isOn: Binding(get: { + timelineAppearance.postActionFixedWidth + }, set: { value in + appearancePatch = TimelinePresentationAppearancePatchHelper.shared.setPostActionFixedWidth( + patch: appearancePatch, + value: value + ) + })) { + Text("post_action_fixed_width") + } } } } @@ -204,6 +215,17 @@ struct MacSidebarMediaAppearanceOverrideGroup: View { Text("appearance_expand_media_size") } + Toggle(isOn: Binding(get: { + timelineAppearance.limitMediaGridToNine + }, set: { value in + appearancePatch = TimelinePresentationAppearancePatchHelper.shared.setLimitMediaGridToNine( + patch: appearancePatch, + value: value + ) + })) { + Text("appearance_limit_media_grid_to_nine") + } + Toggle(isOn: Binding(get: { timelineAppearance.showSensitiveContent }, set: { value in diff --git a/appleApp/macos/App/RootView.swift b/appleApp/macos/App/RootView.swift index d2d21c65fb..ef2fdc9fe6 100644 --- a/appleApp/macos/App/RootView.swift +++ b/appleApp/macos/App/RootView.swift @@ -171,7 +171,7 @@ struct RootView: View { } detail: { if let selectedTab { Router( - initialRoute: selectedTab, + initialRoute: routeWithLatestTimelinePresentation(selectedTab), externalNavigationRequest: mainNavigationRequest ) .navigationSplitViewColumnWidth(min: 280, ideal: 400, max: 500) @@ -257,6 +257,18 @@ struct RootView: View { } } + private func routeWithLatestTimelinePresentation(_ route: Route) -> Route { + guard case .timeline(let selectedTimeline) = route, + case .success(let data) = onEnum(of: homeTimelineWithTabsPresenter.state.tabState), + let latestTimeline = data.data + .cast(UiTimelineTabItem.self) + .first(where: { $0.id == selectedTimeline.id }) + else { + return route + } + return .timeline(latestTimeline) + } + private func handleMainWindowNavigationRequest(_ request: MacMainWindowNavigationRequest?) { guard let request else { return diff --git a/appleApp/macos/UI/FlareTheme.swift b/appleApp/macos/UI/FlareTheme.swift index 6a2357750c..5bcff3133f 100644 --- a/appleApp/macos/UI/FlareTheme.swift +++ b/appleApp/macos/UI/FlareTheme.swift @@ -49,12 +49,14 @@ private extension TimelineAppearance { showSensitiveContent: showSensitiveContent, expandContentWarning: expandContentWarning, expandMediaSize: expandMediaSize, + limitMediaGridToNine: limitMediaGridToNine, videoAutoplay: videoAutoplay, showLinkPreview: showLinkPreview, compatLinkPreview: compatLinkPreview, showNumbers: showNumbers, postActionStyle: postActionStyle, postActionLayout: postActionLayout, + postActionFixedWidth: postActionFixedWidth, fullWidthPost: fullWidthPost, absoluteTimestamp: absoluteTimestamp, showPlatformLogo: showPlatformLogo, diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusComponent.kt index 96c4644166..b3b2e9cace 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusComponent.kt @@ -910,12 +910,11 @@ internal fun StatusActions( verticalAlignment = Alignment.Bottom, horizontalArrangement = when (appearanceSettings.postActionStyle) { - PostActionStyle.Hidden -> Arrangement.Start - PostActionStyle.LeftAligned -> Arrangement.Start - PostActionStyle.RightAligned -> Arrangement.End + PostActionStyle.Hidden -> Arrangement.spacedBy(4.dp, Alignment.Start) + PostActionStyle.LeftAligned -> Arrangement.spacedBy(4.dp, Alignment.Start) + PostActionStyle.RightAligned -> Arrangement.spacedBy(4.dp, Alignment.End) PostActionStyle.Stretch -> Arrangement.SpaceBetween }, -// horizontalArrangement = Arrangement.spacedBy(4.dp), ) { displayItems.fastForEachIndexed { index, action -> if (index == displayItems.lastIndex && appearanceSettings.postActionStyle == PostActionStyle.LeftAligned) { @@ -931,7 +930,10 @@ internal fun StatusActions( color = action.displayItem.color?.toComposeColor() ?: PlatformContentColor.current, - withTextMinWidth = action.displayItem.count != null && index != displayItems.lastIndex, + withTextMinWidth = + appearanceSettings.postActionFixedWidth && + action.displayItem.count != null && + index != displayItems.lastIndex, ) { closeMenu, isMenuShown -> action.actions.fastForEach { subActions -> when (subActions) { @@ -958,7 +960,10 @@ internal fun StatusActions( // Fallback or handle null number = action.count, color = action.color?.toComposeColor() ?: PlatformContentColor.current, - withTextMinWidth = action.count != null && index != displayItems.lastIndex, + withTextMinWidth = + appearanceSettings.postActionFixedWidth && + action.count != null && + index != displayItems.lastIndex, onClicked = { action.onClicked.let { onClick -> haptics.performHapticFeedback(HapticFeedbackType.ContextClick) diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusActionButton.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusActionButton.kt index 0871e67cce..d6f4bd3a06 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusActionButton.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusActionButton.kt @@ -14,10 +14,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -56,13 +54,11 @@ public fun StatusActionButton( ) { val interactionSource = remember { MutableInteractionSource() } val appearanceSettings = LocalTimelineAppearance.current - Row( - modifier = - modifier - .padding(vertical = 4.dp, horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { + val displayNumber = + number?.takeIf { + appearanceSettings.showNumbers && it.humanized.isNotEmpty() + } + val actionIcon: @Composable () -> Unit = { if (!LocalIsScrollingInProgress.current) { val contentColor = PlatformContentColor.current AnimatedContent( @@ -125,35 +121,60 @@ public fun StatusActionButton( tint = color, ) } - if (withTextMinWidth || (number != null && appearanceSettings.showNumbers)) { + } + Row( + modifier = + modifier + .padding(vertical = 4.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + if (withTextMinWidth && displayNumber == null) { Box( - modifier = Modifier.align(Alignment.CenterVertically), + contentAlignment = Alignment.CenterStart, ) { - if (withTextMinWidth) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + FAIcon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.height(PlatformTextStyle.current.fontSize.value.dp + 2.dp), + tint = Color.Transparent, + ) PlatformText( "0000", color = Color.Transparent, ) } - if (number != null && appearanceSettings.showNumbers) { - AnimatedNumber( - number = number, - color = color, - modifier = - Modifier - .pointerHoverIcon(PointerIcon.Hand) - .clickable( - onClick = onClicked, - enabled = enabled, - interactionSource = interactionSource, - indication = null, - ), + actionIcon() + } + } else { + actionIcon() + } + if (displayNumber != null) { + Box( + modifier = Modifier.align(Alignment.CenterVertically), + ) { + if (withTextMinWidth) { + PlatformText( + "0000", + color = Color.Transparent, ) - } else { - if (withTextMinWidth) { - Spacer(modifier = Modifier.width(4.dp)) - } } + AnimatedNumber( + number = displayNumber, + color = color, + modifier = + Modifier + .pointerHoverIcon(PointerIcon.Hand) + .clickable( + onClick = onClicked, + enabled = enabled, + interactionSource = interactionSource, + indication = null, + ), + ) } } } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusMediaComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusMediaComponent.kt index c749be5914..6d588f5408 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusMediaComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/StatusMediaComponent.kt @@ -206,6 +206,7 @@ internal fun StatusMediaComponent( } } }, + maxItems = if (appearanceSettings.limitMediaGridToNine) 9 else data.size, modifier = Modifier .let { diff --git a/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml b/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml index 5d27b200b3..9192c93fbd 100644 --- a/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml +++ b/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml @@ -290,6 +290,8 @@ 总是显示内容警告隐藏的帖子内容 保持媒体比例 在时间轴中按原比例显示媒体 + 媒体网格最多显示 9 项 + 每条时间轴动态最多显示 9 项媒体 视频自动播放 自动播放内容中的视频 仅 Wi-Fi @@ -334,6 +336,8 @@ 动态操作 自定义操作 选择哪些操作显示在按钮行、更多菜单或保持隐藏 + 按钮固定宽度 + 让带数字的操作按钮保持统一宽度 按钮行 更多菜单 隐藏 diff --git a/desktopApp/src/main/composeResources/values/strings.xml b/desktopApp/src/main/composeResources/values/strings.xml index ea181e110a..53072e6a59 100644 --- a/desktopApp/src/main/composeResources/values/strings.xml +++ b/desktopApp/src/main/composeResources/values/strings.xml @@ -326,6 +326,8 @@ Always reveal post content hidden by content warnings Preserve media aspect ratio Show media at its original aspect ratio in timelines + Limit media grids to 9 items + Show at most 9 media items in each timeline post Video autoplay Automatically play videos in posts Wi-Fi only @@ -370,6 +372,8 @@ Post actions Customize actions Choose which actions appear in the row, More menu, or stay hidden + Fixed button width + Keep action buttons with counts at a consistent width Button row More menu Hidden diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/TimelinePresentationEditor.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/TimelinePresentationEditor.kt index 8bea753bc5..b95a8d9b0c 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/TimelinePresentationEditor.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/TimelinePresentationEditor.kt @@ -61,6 +61,8 @@ import dev.dimension.flare.settings_appearance_full_width_post import dev.dimension.flare.settings_appearance_full_width_post_description import dev.dimension.flare.settings_appearance_layout_group_subtitle import dev.dimension.flare.settings_appearance_layout_group_title +import dev.dimension.flare.settings_appearance_limit_media_grid_to_nine +import dev.dimension.flare.settings_appearance_limit_media_grid_to_nine_description import dev.dimension.flare.settings_appearance_media_group_subtitle import dev.dimension.flare.settings_appearance_media_group_title import dev.dimension.flare.settings_appearance_post_action_style @@ -91,6 +93,8 @@ import dev.dimension.flare.settings_appearance_video_autoplay_always import dev.dimension.flare.settings_appearance_video_autoplay_description import dev.dimension.flare.settings_appearance_video_autoplay_never import dev.dimension.flare.settings_appearance_video_autoplay_wifi +import dev.dimension.flare.settings_post_action_fixed_width +import dev.dimension.flare.settings_post_action_fixed_width_description import dev.dimension.flare.tab_settings_filter_content_group import dev.dimension.flare.tab_settings_filter_desc import dev.dimension.flare.tab_settings_filter_image @@ -152,6 +156,7 @@ internal fun TimelinePresentationEditor( appearancePatch.contains(AppearanceKeys.TimelineDisplayMode) || appearancePatch.contains(AppearanceKeys.FullWidthPost) || appearancePatch.contains(AppearanceKeys.PostActionStyle) || + appearancePatch.contains(AppearanceKeys.PostActionFixedWidth) || appearancePatch.contains(AppearanceKeys.ShowNumbers) val displayOverridesEnabled = appearancePatch.contains(AppearanceKeys.AbsoluteTimestamp) || @@ -163,6 +168,7 @@ internal fun TimelinePresentationEditor( appearancePatch.contains(AppearanceKeys.ShowSensitiveContent) || appearancePatch.contains(AppearanceKeys.ExpandContentWarning) || appearancePatch.contains(AppearanceKeys.ExpandMediaSize) || + appearancePatch.contains(AppearanceKeys.LimitMediaGridToNine) || appearancePatch.contains(AppearanceKeys.VideoAutoplay) val themeOverridesEnabled = appearancePatch.contains(AppearanceKeys.AvatarShape) var showFilterDialog by remember { mutableStateOf(false) } @@ -229,11 +235,16 @@ internal fun TimelinePresentationEditor( AppearanceKeys.PostActionStyle, timelineAppearance.postActionStyle, ).set(AppearanceKeys.ShowNumbers, timelineAppearance.showNumbers) + .set( + AppearanceKeys.PostActionFixedWidth, + timelineAppearance.postActionFixedWidth, + ) } else { appearancePatch.clearAll( AppearanceKeys.TimelineDisplayMode, AppearanceKeys.FullWidthPost, AppearanceKeys.PostActionStyle, + AppearanceKeys.PostActionFixedWidth, AppearanceKeys.ShowNumbers, ) }, @@ -310,6 +321,20 @@ internal fun TimelinePresentationEditor( ) }, ) + ExpanderItemSeparator() + SwitchItem( + title = stringResource(Res.string.settings_post_action_fixed_width), + caption = stringResource(Res.string.settings_post_action_fixed_width_description), + checked = timelineAppearance.postActionFixedWidth, + onCheckedChange = { + onAppearancePatchChange( + appearancePatch.set( + AppearanceKeys.PostActionFixedWidth, + it, + ), + ) + }, + ) } } } @@ -425,6 +450,9 @@ internal fun TimelinePresentationEditor( ).set( AppearanceKeys.ExpandMediaSize, timelineAppearance.expandMediaSize, + ).set( + AppearanceKeys.LimitMediaGridToNine, + timelineAppearance.limitMediaGridToNine, ).set( AppearanceKeys.VideoAutoplay, timelineAppearance.videoAutoplay, @@ -435,6 +463,7 @@ internal fun TimelinePresentationEditor( AppearanceKeys.ShowSensitiveContent, AppearanceKeys.ExpandContentWarning, AppearanceKeys.ExpandMediaSize, + AppearanceKeys.LimitMediaGridToNine, AppearanceKeys.VideoAutoplay, ) }, @@ -485,6 +514,20 @@ internal fun TimelinePresentationEditor( }, ) ExpanderItemSeparator() + SwitchItem( + title = stringResource(Res.string.settings_appearance_limit_media_grid_to_nine), + caption = stringResource(Res.string.settings_appearance_limit_media_grid_to_nine_description), + checked = timelineAppearance.limitMediaGridToNine, + onCheckedChange = { + onAppearancePatchChange( + appearancePatch.set( + AppearanceKeys.LimitMediaGridToNine, + it, + ), + ) + }, + ) + ExpanderItemSeparator() SwitchItem( title = stringResource(Res.string.settings_appearance_expand_media), caption = stringResource(Res.string.settings_appearance_expand_media_description), diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt index 9e8a9d0efc..1a5ca4cae6 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt @@ -157,6 +157,8 @@ import dev.dimension.flare.settings_appearance_full_width_post import dev.dimension.flare.settings_appearance_full_width_post_description import dev.dimension.flare.settings_appearance_layout_group_subtitle import dev.dimension.flare.settings_appearance_layout_group_title +import dev.dimension.flare.settings_appearance_limit_media_grid_to_nine +import dev.dimension.flare.settings_appearance_limit_media_grid_to_nine_description import dev.dimension.flare.settings_appearance_media_group_subtitle import dev.dimension.flare.settings_appearance_media_group_title import dev.dimension.flare.settings_appearance_post_action_layout @@ -207,6 +209,8 @@ import dev.dimension.flare.settings_link_open_defaults_title import dev.dimension.flare.settings_local_history_description import dev.dimension.flare.settings_local_history_title import dev.dimension.flare.settings_nostr_relays_manage +import dev.dimension.flare.settings_post_action_fixed_width +import dev.dimension.flare.settings_post_action_fixed_width_description import dev.dimension.flare.settings_privacy_policy import dev.dimension.flare.settings_rss_management_description import dev.dimension.flare.settings_rss_management_title @@ -937,6 +941,24 @@ internal fun SettingsScreen( ) }, ) + ExpanderItemSeparator() + ExpanderItem( + heading = { + Text(stringResource(Res.string.settings_post_action_fixed_width)) + }, + caption = { + Text(stringResource(Res.string.settings_post_action_fixed_width_description)) + }, + trailing = { + Switcher( + checked = LocalTimelineAppearance.current.postActionFixedWidth, + { + state.appearanceState.update(AppearanceKeys.PostActionFixedWidth, it) + }, + textBefore = true, + ) + }, + ) } } } @@ -1101,6 +1123,24 @@ internal fun SettingsScreen( } AnimatedVisibility(LocalTimelineAppearance.current.showMedia) { Column { + ExpanderItemSeparator() + ExpanderItem( + heading = { + Text(stringResource(Res.string.settings_appearance_limit_media_grid_to_nine)) + }, + caption = { + Text(stringResource(Res.string.settings_appearance_limit_media_grid_to_nine_description)) + }, + trailing = { + Switcher( + checked = LocalTimelineAppearance.current.limitMediaGridToNine, + { + state.appearanceState.update(AppearanceKeys.LimitMediaGridToNine, it) + }, + textBefore = true, + ) + }, + ) ExpanderItemSeparator() ExpanderItem( heading = { diff --git a/shared/src/appleMain/kotlin/dev/dimension/flare/data/model/appearance/TimelinePresentationAppearancePatchHelper.kt b/shared/src/appleMain/kotlin/dev/dimension/flare/data/model/appearance/TimelinePresentationAppearancePatchHelper.kt index 1fe4f64000..4ffebe825e 100644 --- a/shared/src/appleMain/kotlin/dev/dimension/flare/data/model/appearance/TimelinePresentationAppearancePatchHelper.kt +++ b/shared/src/appleMain/kotlin/dev/dimension/flare/data/model/appearance/TimelinePresentationAppearancePatchHelper.kt @@ -17,6 +17,7 @@ public object TimelinePresentationAppearancePatchHelper { patch.contains(AppearanceKeys.TimelineDisplayMode) || patch.contains(AppearanceKeys.FullWidthPost) || patch.contains(AppearanceKeys.PostActionStyle) || + patch.contains(AppearanceKeys.PostActionFixedWidth) || patch.contains(AppearanceKeys.ShowNumbers) public fun displayOverridesEnabled(patch: AppearancePatch): Boolean = @@ -30,6 +31,7 @@ public object TimelinePresentationAppearancePatchHelper { patch.contains(AppearanceKeys.ShowSensitiveContent) || patch.contains(AppearanceKeys.ExpandContentWarning) || patch.contains(AppearanceKeys.ExpandMediaSize) || + patch.contains(AppearanceKeys.LimitMediaGridToNine) || patch.contains(AppearanceKeys.VideoAutoplay) public fun themeOverridesEnabled(patch: AppearancePatch): Boolean = patch.contains(AppearanceKeys.AvatarShape) @@ -42,6 +44,7 @@ public object TimelinePresentationAppearancePatchHelper { .set(AppearanceKeys.TimelineDisplayMode, appearance.timelineDisplayMode) .set(AppearanceKeys.FullWidthPost, appearance.fullWidthPost) .set(AppearanceKeys.PostActionStyle, appearance.postActionStyle) + .set(AppearanceKeys.PostActionFixedWidth, appearance.postActionFixedWidth) .set(AppearanceKeys.ShowNumbers, appearance.showNumbers) public fun disableLayoutOverrides(patch: AppearancePatch): AppearancePatch = @@ -49,6 +52,7 @@ public object TimelinePresentationAppearancePatchHelper { .clear(AppearanceKeys.TimelineDisplayMode) .clear(AppearanceKeys.FullWidthPost) .clear(AppearanceKeys.PostActionStyle) + .clear(AppearanceKeys.PostActionFixedWidth) .clear(AppearanceKeys.ShowNumbers) public fun enableDisplayOverrides( @@ -77,6 +81,7 @@ public object TimelinePresentationAppearancePatchHelper { .set(AppearanceKeys.ShowSensitiveContent, appearance.showSensitiveContent) .set(AppearanceKeys.ExpandContentWarning, appearance.expandContentWarning) .set(AppearanceKeys.ExpandMediaSize, appearance.expandMediaSize) + .set(AppearanceKeys.LimitMediaGridToNine, appearance.limitMediaGridToNine) .set(AppearanceKeys.VideoAutoplay, appearance.videoAutoplay) public fun disableMediaOverrides(patch: AppearancePatch): AppearancePatch = @@ -85,6 +90,7 @@ public object TimelinePresentationAppearancePatchHelper { .clear(AppearanceKeys.ShowSensitiveContent) .clear(AppearanceKeys.ExpandContentWarning) .clear(AppearanceKeys.ExpandMediaSize) + .clear(AppearanceKeys.LimitMediaGridToNine) .clear(AppearanceKeys.VideoAutoplay) public fun enableThemeOverrides( @@ -109,6 +115,11 @@ public object TimelinePresentationAppearancePatchHelper { value: PostActionStyle, ): AppearancePatch = patch.set(AppearanceKeys.PostActionStyle, value) + public fun setPostActionFixedWidth( + patch: AppearancePatch, + value: Boolean, + ): AppearancePatch = patch.set(AppearanceKeys.PostActionFixedWidth, value) + public fun setShowNumbers( patch: AppearancePatch, value: Boolean, @@ -154,6 +165,11 @@ public object TimelinePresentationAppearancePatchHelper { value: Boolean, ): AppearancePatch = patch.set(AppearanceKeys.ExpandMediaSize, value) + public fun setLimitMediaGridToNine( + patch: AppearancePatch, + value: Boolean, + ): AppearancePatch = patch.set(AppearanceKeys.LimitMediaGridToNine, value) + public fun setVideoAutoplay( patch: AppearancePatch, value: VideoAutoplay, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceKeys.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceKeys.kt index a3da0a7243..2b0e6ca3bb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceKeys.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceKeys.kt @@ -60,6 +60,12 @@ public object AppearanceKeys { public object ExpandMediaSize : PerTimeline("timeline.expand_media_size", true, Boolean.serializer()) + public object LimitMediaGridToNine : PerTimeline( + "timeline.limit_media_grid_to_nine", + true, + Boolean.serializer(), + ) + public object VideoAutoplay : PerTimeline( "timeline.video_autoplay", AppearanceVideoAutoplay.WIFI, @@ -84,6 +90,12 @@ public object AppearanceKeys { AppearancePostActionLayoutConfig.serializer(), ) + public object PostActionFixedWidth : PerTimeline( + "timeline.post_action_fixed_width", + true, + Boolean.serializer(), + ) + public object FullWidthPost : PerTimeline("timeline.full_width_post", false, Boolean.serializer()) public object AbsoluteTimestamp : PerTimeline("timeline.absolute_timestamp", false, Boolean.serializer()) @@ -115,12 +127,14 @@ public object AppearanceKeys { ShowSensitiveContent, ExpandContentWarning, ExpandMediaSize, + LimitMediaGridToNine, VideoAutoplay, ShowLinkPreview, CompatLinkPreview, ShowNumbers, PostActionStyle, PostActionLayout, + PostActionFixedWidth, FullWidthPost, AbsoluteTimestamp, ShowPlatformLogo, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceModels.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceModels.kt index 31e6f86be9..1c1e9ea04c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceModels.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/model/appearance/AppearanceModels.kt @@ -37,12 +37,14 @@ public data class TimelineAppearance( val showSensitiveContent: Boolean = AppearanceKeys.ShowSensitiveContent.default, val expandContentWarning: Boolean = AppearanceKeys.ExpandContentWarning.default, val expandMediaSize: Boolean = AppearanceKeys.ExpandMediaSize.default, + val limitMediaGridToNine: Boolean = AppearanceKeys.LimitMediaGridToNine.default, val videoAutoplay: VideoAutoplay = AppearanceKeys.VideoAutoplay.default, val showLinkPreview: Boolean = AppearanceKeys.ShowLinkPreview.default, val compatLinkPreview: Boolean = AppearanceKeys.CompatLinkPreview.default, val showNumbers: Boolean = AppearanceKeys.ShowNumbers.default, val postActionStyle: PostActionStyle = AppearanceKeys.PostActionStyle.default, val postActionLayout: PostActionLayoutConfig = AppearanceKeys.PostActionLayout.default, + val postActionFixedWidth: Boolean = AppearanceKeys.PostActionFixedWidth.default, val fullWidthPost: Boolean = AppearanceKeys.FullWidthPost.default, val absoluteTimestamp: Boolean = AppearanceKeys.AbsoluteTimestamp.default, val showPlatformLogo: Boolean = AppearanceKeys.ShowPlatformLogo.default, @@ -87,12 +89,14 @@ public fun AppearancePatch.toTimelineAppearance(override: AppearancePatch?): Tim showSensitiveContent = getTimelineValue(AppearanceKeys.ShowSensitiveContent, override), expandContentWarning = getTimelineValue(AppearanceKeys.ExpandContentWarning, override), expandMediaSize = getTimelineValue(AppearanceKeys.ExpandMediaSize, override), + limitMediaGridToNine = getTimelineValue(AppearanceKeys.LimitMediaGridToNine, override), videoAutoplay = getTimelineValue(AppearanceKeys.VideoAutoplay, override), showLinkPreview = getTimelineValue(AppearanceKeys.ShowLinkPreview, override), compatLinkPreview = getTimelineValue(AppearanceKeys.CompatLinkPreview, override), showNumbers = getTimelineValue(AppearanceKeys.ShowNumbers, override), postActionStyle = getTimelineValue(AppearanceKeys.PostActionStyle, override), postActionLayout = getTimelineValue(AppearanceKeys.PostActionLayout, override), + postActionFixedWidth = getTimelineValue(AppearanceKeys.PostActionFixedWidth, override), fullWidthPost = getTimelineValue(AppearanceKeys.FullWidthPost, override), absoluteTimestamp = getTimelineValue(AppearanceKeys.AbsoluteTimestamp, override), showPlatformLogo = getTimelineValue(AppearanceKeys.ShowPlatformLogo, override), @@ -107,12 +111,14 @@ public fun TimelineAppearance.withPatch(patch: AppearancePatch?): TimelineAppear showSensitiveContent = patch.getTimelineValue(AppearanceKeys.ShowSensitiveContent, showSensitiveContent), expandContentWarning = patch.getTimelineValue(AppearanceKeys.ExpandContentWarning, expandContentWarning), expandMediaSize = patch.getTimelineValue(AppearanceKeys.ExpandMediaSize, expandMediaSize), + limitMediaGridToNine = patch.getTimelineValue(AppearanceKeys.LimitMediaGridToNine, limitMediaGridToNine), videoAutoplay = patch.getTimelineValue(AppearanceKeys.VideoAutoplay, videoAutoplay), showLinkPreview = patch.getTimelineValue(AppearanceKeys.ShowLinkPreview, showLinkPreview), compatLinkPreview = patch.getTimelineValue(AppearanceKeys.CompatLinkPreview, compatLinkPreview), showNumbers = patch.getTimelineValue(AppearanceKeys.ShowNumbers, showNumbers), postActionStyle = patch.getTimelineValue(AppearanceKeys.PostActionStyle, postActionStyle), postActionLayout = patch.getTimelineValue(AppearanceKeys.PostActionLayout, postActionLayout), + postActionFixedWidth = patch.getTimelineValue(AppearanceKeys.PostActionFixedWidth, postActionFixedWidth), fullWidthPost = patch.getTimelineValue(AppearanceKeys.FullWidthPost, fullWidthPost), absoluteTimestamp = patch.getTimelineValue(AppearanceKeys.AbsoluteTimestamp, absoluteTimestamp), showPlatformLogo = patch.getTimelineValue(AppearanceKeys.ShowPlatformLogo, showPlatformLogo), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/SettingsPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/SettingsPresenter.kt index b72d52fd88..82521c88eb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/SettingsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/SettingsPresenter.kt @@ -84,6 +84,8 @@ public class SettingsPresenter : PresenterBase() { override fun updateExpandMediaSize(value: Boolean) = update(AppearanceKeys.ExpandMediaSize, value) + override fun updateLimitMediaGridToNine(value: Boolean) = update(AppearanceKeys.LimitMediaGridToNine, value) + override fun updateShowSensitiveContent(value: Boolean) = update(AppearanceKeys.ShowSensitiveContent, value) override fun updateExpandContentWarning(value: Boolean) = update(AppearanceKeys.ExpandContentWarning, value) @@ -102,6 +104,8 @@ public class SettingsPresenter : PresenterBase() { override fun updatePostActionLayout(value: PostActionLayoutConfig) = update(AppearanceKeys.PostActionLayout, value) + override fun updatePostActionFixedWidth(value: Boolean) = update(AppearanceKeys.PostActionFixedWidth, value) + override fun updateShowNumbers(value: Boolean) = update(AppearanceKeys.ShowNumbers, value) override fun updateAppSettings(block: AppSettings.() -> AppSettings) { @@ -150,6 +154,8 @@ public class SettingsPresenter : PresenterBase() { public fun updateExpandMediaSize(value: Boolean) + public fun updateLimitMediaGridToNine(value: Boolean) + public fun updateShowSensitiveContent(value: Boolean) public fun updateExpandContentWarning(value: Boolean) @@ -168,6 +174,8 @@ public class SettingsPresenter : PresenterBase() { public fun updatePostActionLayout(value: PostActionLayoutConfig) + public fun updatePostActionFixedWidth(value: Boolean) + public fun updateShowNumbers(value: Boolean) @WebIgnore diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/model/appearance/AppearancePatchTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/model/appearance/AppearancePatchTest.kt index 7829133c41..46e09f5eb9 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/data/model/appearance/AppearancePatchTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/model/appearance/AppearancePatchTest.kt @@ -28,6 +28,7 @@ class AppearancePatchTest { fun emptyPatchSynthesizesDefaultAppearanceSettings() { assertEquals(AppearanceSettings.Default, AppearancePatch.EMPTY.toAppearanceSettings()) assertFalse(TimelineAppearance.Default.expandContentWarning) + assertEquals(true, TimelineAppearance.Default.limitMediaGridToNine) } @Test @@ -54,6 +55,8 @@ class AppearancePatchTest { .set(AppearanceKeys.AvatarShape, AvatarShape.SQUARE) .set(AppearanceKeys.ShowMedia, false) .set(AppearanceKeys.ExpandContentWarning, true) + .set(AppearanceKeys.LimitMediaGridToNine, false) + .set(AppearanceKeys.PostActionFixedWidth, false) .set(AppearanceKeys.TimelineDisplayMode, TimelineDisplayMode.Gallery) assertEquals( @@ -69,6 +72,8 @@ class AppearancePatchTest { avatarShape = AvatarShape.SQUARE, showMedia = false, expandContentWarning = true, + limitMediaGridToNine = false, + postActionFixedWidth = false, timelineDisplayMode = TimelineDisplayMode.Gallery, ), patch.toTimelineAppearance(), @@ -81,6 +86,7 @@ class AppearancePatchTest { AppearancePatch.EMPTY .set(AppearanceKeys.ShowMedia, false) .set(AppearanceKeys.ShowNumbers, false) + .set(AppearanceKeys.PostActionFixedWidth, false) .set(AppearanceKeys.ExpandContentWarning, false) val timelinePatch = AppearancePatch.EMPTY @@ -92,6 +98,7 @@ class AppearancePatchTest { showMedia = true, expandContentWarning = true, showNumbers = false, + postActionFixedWidth = false, ), globalPatch.toTimelineAppearance(timelinePatch), ) @@ -111,13 +118,17 @@ class AppearancePatchTest { AppearancePatch.EMPTY .set(AppearanceKeys.ShowMedia, true) .set(AppearanceKeys.ExpandContentWarning, true) + .set(AppearanceKeys.LimitMediaGridToNine, false) + .set(AppearanceKeys.PostActionFixedWidth, false) .set(AppearanceKeys.TimelineDisplayMode, TimelineDisplayMode.Gallery) assertEquals( TimelineAppearance( showMedia = true, expandContentWarning = true, + limitMediaGridToNine = false, showNumbers = false, + postActionFixedWidth = false, timelineDisplayMode = TimelineDisplayMode.Gallery, aiConfig = TimelineAppearance.AiConfig(translation = true, tldr = true), lineLimit = 9, @@ -171,7 +182,9 @@ class AppearancePatchTest { AppearanceKeys.ShowBottomBarLabels, AppearanceKeys.DeckMode, AppearanceKeys.ExpandContentWarning, + AppearanceKeys.LimitMediaGridToNine, AppearanceKeys.PostActionLayout, + AppearanceKeys.PostActionFixedWidth, ) val activeFields = AppearanceSettings @@ -227,8 +240,10 @@ class AppearancePatchTest { .set(AppearanceKeys.DeckMode, true) .set(AppearanceKeys.ShowMedia, false) .set(AppearanceKeys.ExpandContentWarning, true) + .set(AppearanceKeys.LimitMediaGridToNine, false) .set(AppearanceKeys.VideoAutoplay, VideoAutoplay.ALWAYS) .set(AppearanceKeys.PostActionStyle, PostActionStyle.Stretch) + .set(AppearanceKeys.PostActionFixedWidth, false) .set( AppearanceKeys.PostActionLayout, PostActionLayoutConfig( diff --git a/web/src/lib/components/UiTimeline/post/postUtils.ts b/web/src/lib/components/UiTimeline/post/postUtils.ts index d8fde1f037..1e0a6a0145 100644 --- a/web/src/lib/components/UiTimeline/post/postUtils.ts +++ b/web/src/lib/components/UiTimeline/post/postUtils.ts @@ -21,6 +21,7 @@ export const defaultTimelineAppearance = { expandMediaSize: true, fullWidthPost: false, lineLimit: 5, + postActionFixedWidth: true, postActionStyle: 'LeftAligned', showLinkPreview: true, showMedia: true, diff --git a/web/src/routes/tabs/+page.svelte b/web/src/routes/tabs/+page.svelte index 3a9dfaec36..1619fb352a 100644 --- a/web/src/routes/tabs/+page.svelte +++ b/web/src/routes/tabs/+page.svelte @@ -76,6 +76,7 @@ compatLinkPreview: false, showNumbers: true, postActionStyle: 'LeftAligned', + postActionFixedWidth: true, fullWidthPost: false, absoluteTimestamp: false, showPlatformLogo: true,