mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 08:20:59 +00:00
Tag groups (#1506)
* Implemented tag groups * Cleanup --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
parent
c4c705aa10
commit
5951bcec38
32 changed files with 553 additions and 37 deletions
|
@ -105,6 +105,8 @@
|
|||
E9DF420129830FEC0003AAD2 /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DF420029830FEC0003AAD2 /* ActionRequestHandler.swift */; };
|
||||
E9DF420329830FEC0003AAD2 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = E9DF420229830FEC0003AAD2 /* Action.js */; };
|
||||
E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
FA31A9AB2A66BF7C00D5F662 /* AddTagGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */; };
|
||||
FAD203D02A66D8A80030A7FD /* Symbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD203CF2A66D8A80030A7FD /* Symbols.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -272,6 +274,8 @@
|
|||
E9DF420229830FEC0003AAD2 /* Action.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = "<group>"; };
|
||||
E9DF420429830FEC0003AAD2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F355EEDA297A8BD500E362C0 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddTagGroupView.swift; sourceTree = "<group>"; };
|
||||
FAD203CF2A66D8A80030A7FD /* Symbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Symbols.swift; sourceTree = "<group>"; };
|
||||
FF8259FB298E42E000BEAB69 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
FF8259FC298E42E000BEAB69 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
@ -401,6 +405,8 @@
|
|||
children = (
|
||||
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
||||
9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */,
|
||||
FA31A9AA2A66BF7C00D5F662 /* AddTagGroupView.swift */,
|
||||
FAD203CF2A66D8A80030A7FD /* Symbols.swift */,
|
||||
);
|
||||
path = Timeline;
|
||||
sourceTree = "<group>";
|
||||
|
@ -829,6 +835,8 @@
|
|||
9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */,
|
||||
9F2A540729699698009B2D7C /* SupportAppView.swift in Sources */,
|
||||
9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */,
|
||||
FA31A9AB2A66BF7C00D5F662 /* AddTagGroupView.swift in Sources */,
|
||||
FAD203D02A66D8A80030A7FD /* Symbols.swift in Sources */,
|
||||
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
|
||||
9F398AA62935FE8A00A889F2 /* AppRouter.swift in Sources */,
|
||||
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
||||
|
|
|
@ -82,6 +82,9 @@ extension View {
|
|||
case .addRemoteLocalTimeline:
|
||||
AddRemoteTimelineView()
|
||||
.withEnvironments()
|
||||
case .addTagGroup:
|
||||
AddTagGroupView()
|
||||
.withEnvironments()
|
||||
case let .statusEditHistory(status):
|
||||
StatusEditHistoryView(statusId: status)
|
||||
.withEnvironments()
|
||||
|
|
|
@ -139,6 +139,9 @@ struct SettingsTabs: View {
|
|||
NavigationLink(destination: remoteLocalTimelinesView) {
|
||||
Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
NavigationLink(destination: tagGroupsView) {
|
||||
Label("timeline.filter.tag-groups", systemImage: "number")
|
||||
}
|
||||
NavigationLink(destination: ContentSettingsView()) {
|
||||
Label("settings.general.content", systemImage: "rectangle.stack")
|
||||
}
|
||||
|
@ -261,6 +264,36 @@ struct SettingsTabs: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var tagGroupsView: some View {
|
||||
Form {
|
||||
ForEach(preferences.tagGroups, id: \.self) { group in
|
||||
Text(group.title)
|
||||
}
|
||||
.onDelete { indexes in
|
||||
if let index = indexes.first {
|
||||
_ = preferences.tagGroups.remove(at: index)
|
||||
}
|
||||
}
|
||||
.onMove { source, destination in
|
||||
preferences.tagGroups.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
Button {
|
||||
routerPath.presentedSheet = .addTagGroup
|
||||
} label: {
|
||||
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.navigationTitle("timeline.filter.tag-groups")
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
|
||||
private var remoteLocalTimelinesView: some View {
|
||||
Form {
|
||||
|
|
197
IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift
Normal file
197
IceCubesApp/App/Tabs/Timeline/AddTagGroupView.swift
Normal file
|
@ -0,0 +1,197 @@
|
|||
import Combine
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NukeUI
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
struct AddTagGroupView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@State private var title: String = ""
|
||||
@State private var sfSymbolName: String = ""
|
||||
@State private var tags: [String] = []
|
||||
@State private var newTag: String = ""
|
||||
|
||||
private var canSave: Bool {
|
||||
!title.isEmpty &&
|
||||
// At least have 2 tags, one main and one additional.
|
||||
tags.count >= 2
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: Focus?
|
||||
|
||||
enum Focus {
|
||||
case title
|
||||
case symbol
|
||||
case new
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
metadataSection
|
||||
keywordsSection
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle("timeline.filter.add-tag-groups")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("action.cancel", action: { dismiss() })
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.save", action: { save() })
|
||||
.disabled(!canSave)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
focusedField = .title
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
symbolsSuggestionView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var metadataSection: some View {
|
||||
Section {
|
||||
TextField("add-tag-groups.edit.title.field", text: $title, axis: .horizontal)
|
||||
.focused($focusedField, equals: Focus.title)
|
||||
.onSubmit {
|
||||
focusedField = Focus.symbol
|
||||
}
|
||||
|
||||
HStack {
|
||||
TextField("add-tag-groups.edit.icon.field", text: $sfSymbolName, axis: .horizontal)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($focusedField, equals: Focus.symbol)
|
||||
.onSubmit {
|
||||
focusedField = Focus.new
|
||||
}
|
||||
.onChange(of: sfSymbolName) { name in
|
||||
popupTagsPresented = true
|
||||
}
|
||||
|
||||
Image(systemName: sfSymbolName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var popupTagsPresented = false
|
||||
|
||||
private var keywordsSection: some View {
|
||||
Section("add-tag-groups.edit.tags") {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
HStack {
|
||||
Text(tag)
|
||||
Spacer()
|
||||
Button {
|
||||
deleteTag(tag)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { indexes in
|
||||
if let index = indexes.first {
|
||||
let tag = tags[index]
|
||||
deleteTag(tag)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
TextField("add-tag-groups.edit.tags.add", text: $newTag, axis: .horizontal)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.onSubmit {
|
||||
addNewTag()
|
||||
}
|
||||
.focused($focusedField, equals: Focus.new)
|
||||
Spacer()
|
||||
if !newTag.isEmpty {
|
||||
Button {
|
||||
addNewTag()
|
||||
} label: {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.tint(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
private func addNewTag() {
|
||||
addTag(newTag.trimmingCharacters(in: .whitespaces))
|
||||
newTag = ""
|
||||
focusedField = Focus.new
|
||||
}
|
||||
|
||||
private func addTag(_ tag: String) {
|
||||
guard !tag.isEmpty else { return }
|
||||
tags.append(tag)
|
||||
}
|
||||
|
||||
private func deleteTag(_ tag: String) {
|
||||
tags.removeAll(where: { $0 == tag })
|
||||
}
|
||||
|
||||
private func save() {
|
||||
var toSave = tags
|
||||
let main = toSave.removeFirst()
|
||||
preferences.tagGroups.append(.init(
|
||||
title: title.trimmingCharacters(in: .whitespaces),
|
||||
sfSymbolName: sfSymbolName,
|
||||
main: main,
|
||||
additional: toSave
|
||||
))
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var symbolsSuggestionView: some View {
|
||||
if focusedField == .symbol && !sfSymbolName.isEmpty {
|
||||
let filteredMatches = allSymbols
|
||||
.filter { $0.contains(sfSymbolName) }
|
||||
if !filteredMatches.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(filteredMatches, id: \.self) { symbolName in
|
||||
Button {
|
||||
sfSymbolName = symbolName
|
||||
} label: {
|
||||
Image(systemName: symbolName)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct AddTagGroupView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddTagGroupView()
|
||||
.withEnvironments()
|
||||
}
|
||||
}
|
19
IceCubesApp/App/Tabs/Timeline/Symbols.swift
Normal file
19
IceCubesApp/App/Tabs/Timeline/Symbols.swift
Normal file
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// Symbols.swift
|
||||
// IceCubesApp
|
||||
//
|
||||
// Created by Alejandro Martinez on 18/7/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
let allSymbols: [String] = {
|
||||
if let bundle = Bundle(identifier: "com.apple.CoreGlyphs"),
|
||||
let resourcePath = bundle.path(forResource: "symbol_search", ofType: "plist"),
|
||||
let plist = NSDictionary(contentsOfFile: resourcePath) {
|
||||
|
||||
return plist.allKeys as? [String] ?? []
|
||||
}
|
||||
return []
|
||||
}()
|
|
@ -151,6 +151,25 @@ struct TimelineTab: View {
|
|||
Label("timeline.filter.add-local", systemImage: "badge.plus.radiowaves.right")
|
||||
}
|
||||
}
|
||||
|
||||
Menu("timeline.filter.tag-groups") {
|
||||
ForEach(preferences.tagGroups, id: \.self) { group in
|
||||
Button {
|
||||
timeline = .tagGroup(group)
|
||||
} label: {
|
||||
VStack {
|
||||
let icon = group.sfSymbolName.isEmpty ? "number" : group.sfSymbolName
|
||||
Label(group.title, systemImage: icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
routerPath.presentedSheet = .addTagGroup
|
||||
} label: {
|
||||
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var addAccountButton: some View {
|
||||
|
|
|
@ -245,6 +245,8 @@
|
|||
"timeline.filter.lists" = "Спісы";
|
||||
"timeline.filter.local" = "Мясцовыя шкалы часу";
|
||||
"timeline.filter.tags" = "Адсочваемыя тэгі";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Дадаць уліковы запіс";
|
||||
|
@ -603,3 +605,9 @@
|
|||
|
||||
"tag.suggested.mentions-%lld" = "%lld mentions";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
||||
|
|
|
@ -239,6 +239,8 @@
|
|||
"timeline.filter.lists" = "Llistes";
|
||||
"timeline.filter.local" = "Línies de temps locals";
|
||||
"timeline.filter.tags" = "Etiquetes seguides";
|
||||
"timeline.filter.tag-groups" = "Grups d'Etiquetes";
|
||||
"timeline.filter.add-tag-groups" = "Afegeix grup";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Afegeix un compte";
|
||||
|
@ -596,3 +598,15 @@
|
|||
"status.action.report" = "Report Post";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld mentions";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Títol del Grup d'Etiquetes";
|
||||
"add-tag-groups.edit.icon.field" = "Icona del Grup d'Etiquetes (SFSymbol)";
|
||||
"add-tag-groups.edit.tags" = "Afegeix etiquetes al grup";
|
||||
"add-tag-groups.edit.tags.add" = "Etiqueta";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -242,6 +242,8 @@
|
|||
"timeline.filter.lists" = "Listen";
|
||||
"timeline.filter.local" = "Lokale Timelines";
|
||||
"timeline.filter.tags" = "Gefolgte Hashtags";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Konto hinzufügen";
|
||||
|
@ -583,3 +585,9 @@
|
|||
"report.title" = "Beitrag melden";
|
||||
"report.action.send" = "Absenden";
|
||||
"status.action.report" = "Beitrag melden";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -243,6 +243,8 @@
|
|||
"timeline.filter.lists" = "Lists";
|
||||
"timeline.filter.local" = "Local Timelines";
|
||||
"timeline.filter.tags" = "Followed Tags";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Add Account";
|
||||
|
@ -597,3 +599,9 @@
|
|||
"status.action.report" = "Report Post";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld mentions";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -241,6 +241,8 @@
|
|||
"timeline.filter.lists" = "Lists";
|
||||
"timeline.filter.local" = "Local Timelines";
|
||||
"timeline.filter.tags" = "Followed Tags";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Add Account";
|
||||
|
@ -599,3 +601,9 @@
|
|||
"status.action.report" = "Report Post";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld mentions";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -241,6 +241,8 @@
|
|||
"timeline.filter.lists" = "Listas";
|
||||
"timeline.filter.local" = "Cronologías locales";
|
||||
"timeline.filter.tags" = "Etiquetas que sigues";
|
||||
"timeline.filter.tag-groups" = "Grupos de Etiquetas";
|
||||
"timeline.filter.add-tag-groups" = "Añadir grupo";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Añadir cuenta";
|
||||
|
@ -598,3 +600,9 @@
|
|||
"status.action.report" = "Denunciar publicación";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld menciones";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Título del Grupo de Etiquetas";
|
||||
"add-tag-groups.edit.icon.field" = "Icono del Grupo de Etiquetas (SFSymbol)";
|
||||
"add-tag-groups.edit.tags" = "Añade etiquetas al grupo";
|
||||
"add-tag-groups.edit.tags.add" = "Etiqueta";
|
||||
|
|
|
@ -242,6 +242,8 @@
|
|||
"timeline.filter.lists" = "Zerrendak";
|
||||
"timeline.filter.local" = "Denbora-lerro lokalak";
|
||||
"timeline.filter.tags" = "Jarraitutako traolak";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Gehitu kontua";
|
||||
|
@ -584,3 +586,9 @@
|
|||
"report.title" = "Salaketa";
|
||||
"report.action.send" = "Bidali";
|
||||
"status.action.report" = "Salatu edukia";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -240,6 +240,8 @@
|
|||
"timeline.filter.lists" = "Listes";
|
||||
"timeline.filter.local" = "Chronologies locales";
|
||||
"timeline.filter.tags" = "Tags suivis";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Ajouter un compte";
|
||||
|
@ -593,3 +595,9 @@
|
|||
"status.action.report" = "Signaler la publication";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld mentions";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -239,6 +239,8 @@
|
|||
"timeline.filter.lists" = "Liste";
|
||||
"timeline.filter.local" = "Timeline locali";
|
||||
"timeline.filter.tags" = "Tag seguiti";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Aggiungi account";
|
||||
|
@ -597,3 +599,9 @@
|
|||
"status.action.report" = "Segnala il messaggio";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld mentions";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -240,6 +240,8 @@
|
|||
"timeline.filter.lists" = "リスト";
|
||||
"timeline.filter.local" = "ローカルタイムライン";
|
||||
"timeline.filter.tags" = "フォローしたタグ";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "アカウントの追加";
|
||||
|
@ -596,3 +598,9 @@
|
|||
"status.action.report" = "投稿を報告";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "返信:%lld";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -241,6 +241,8 @@
|
|||
"timeline.filter.local" = "원격 로컬 타임라인";
|
||||
"timeline.filter.tags" = "팔로우한 태그";
|
||||
"timeline-new-posts %lld" = "%lld개 새 글";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "계정 추가";
|
||||
|
@ -600,3 +602,9 @@
|
|||
"status.action.report" = "글 신고";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld개 글";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -240,6 +240,8 @@
|
|||
"timeline.filter.lists" = "Lister";
|
||||
"timeline.filter.local" = "Lokale tidslinjer";
|
||||
"timeline.filter.tags" = "Fulgte tagger";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Legg til konto";
|
||||
|
@ -597,3 +599,9 @@
|
|||
"status.action.report" = "Rapporter innlegg";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld omtaler";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -237,6 +237,8 @@
|
|||
"timeline.filter.lists" = "Lijsten";
|
||||
"timeline.filter.local" = "Lokale tijdlijnen";
|
||||
"timeline.filter.tags" = "Gevolgde hashtags";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Account toevoegen";
|
||||
|
@ -594,3 +596,9 @@
|
|||
"status.action.report" = "Meld post";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld vermeldingen";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -240,6 +240,8 @@
|
|||
"timeline.filter.lists" = "Listy";
|
||||
"timeline.filter.local" = "Strumienie lokalne";
|
||||
"timeline.filter.tags" = "Obserwowane hasztagi";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Dodaj konto";
|
||||
|
@ -587,3 +589,9 @@
|
|||
"report.action.send" = "Wyślij";
|
||||
"status.action.report" = "Zgłoś post";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
||||
|
|
|
@ -240,6 +240,8 @@
|
|||
"timeline.filter.lists" = "Listas";
|
||||
"timeline.filter.local" = "Timelines Locais";
|
||||
"timeline.filter.tags" = "Hashtags Seguidas";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Adicionar Conta";
|
||||
|
@ -597,3 +599,9 @@
|
|||
"status.action.report" = "Denunciar";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld menções";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -240,6 +240,8 @@
|
|||
"timeline.filter.lists" = "Listeler";
|
||||
"timeline.filter.local" = "Yerel Zaman Dilimleri";
|
||||
"timeline.filter.tags" = "Takip Edilen Etiketler";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Hesap Ekle";
|
||||
|
@ -597,3 +599,9 @@
|
|||
"status.action.report" = "Report Post";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld mentions";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -241,6 +241,8 @@
|
|||
"timeline.filter.lists" = "Списки";
|
||||
"timeline.filter.local" = "Локальна стрічка";
|
||||
"timeline.filter.tags" = "Хештеґи";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "Додати обліковий запис";
|
||||
|
@ -599,3 +601,9 @@
|
|||
|
||||
"tag.suggested.mentions-%lld" = "%lld згадок";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
||||
|
|
|
@ -238,6 +238,8 @@
|
|||
"timeline.filter.lists" = "列表";
|
||||
"timeline.filter.local" = "远程时间线";
|
||||
"timeline.filter.tags" = "关注的标签";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "添加账户";
|
||||
|
@ -597,3 +599,9 @@
|
|||
"status.action.report" = "举报嘟文";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld 个提及";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -242,6 +242,8 @@
|
|||
"timeline.filter.lists" = "列表";
|
||||
"timeline.filter.local" = "本站時間軸";
|
||||
"timeline.filter.tags" = "跟隨標籤";
|
||||
"timeline.filter.tag-groups" = "Tag Groups";
|
||||
"timeline.filter.add-tag-groups" = "Add tag group";
|
||||
|
||||
// MARK: Package: AppAccount
|
||||
"app-account.button.add" = "新增帳號";
|
||||
|
@ -599,3 +601,9 @@
|
|||
"status.action.report" = "檢舉嘟文";
|
||||
|
||||
"tag.suggested.mentions-%lld" = "%lld 提及";
|
||||
|
||||
// MARK: Tag Groups
|
||||
"add-tag-groups.edit.title.field" = "Tag Group Title";
|
||||
"add-tag-groups.edit.icon.field" = "Tag Group Icon (SFSymbol name)";
|
||||
"add-tag-groups.edit.tags" = "Add tags to the group";
|
||||
"add-tag-groups.edit.tags.add" = "Tag";
|
||||
|
|
|
@ -33,6 +33,7 @@ public enum SheetDestination: Identifiable {
|
|||
case listAddAccount(account: Account)
|
||||
case addAccount
|
||||
case addRemoteLocalTimeline
|
||||
case addTagGroup
|
||||
case statusEditHistory(status: String)
|
||||
case settings
|
||||
case accountPushNotficationsSettings
|
||||
|
@ -50,6 +51,8 @@ public enum SheetDestination: Identifiable {
|
|||
return "listAddAccount"
|
||||
case .addAccount:
|
||||
return "addAccount"
|
||||
case .addTagGroup:
|
||||
return "addTagGroup"
|
||||
case .addRemoteLocalTimeline:
|
||||
return "addRemoteLocalTimeline"
|
||||
case .statusEditHistory:
|
||||
|
|
|
@ -12,6 +12,7 @@ public class UserPreferences: ObservableObject {
|
|||
private var client: Client?
|
||||
|
||||
@AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = []
|
||||
@AppStorage("tag_groups") public var tagGroups: [TagGroup] = []
|
||||
@AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari
|
||||
@AppStorage("draft_posts") public var draftsPosts: [String] = []
|
||||
@AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true
|
||||
|
|
|
@ -62,3 +62,29 @@ public struct FeaturedTag: Codable, Identifiable {
|
|||
extension Tag: Sendable {}
|
||||
extension Tag.History: Sendable {}
|
||||
extension FeaturedTag: Sendable {}
|
||||
|
||||
|
||||
public struct TagGroup: Codable, Equatable, Hashable {
|
||||
public init(title: String, sfSymbolName: String, main: String, additional: [String]) {
|
||||
self.title = title
|
||||
self.sfSymbolName = sfSymbolName
|
||||
self.main = main
|
||||
self.additional = additional
|
||||
}
|
||||
|
||||
public let title: String
|
||||
public let sfSymbolName: String
|
||||
public let main: String
|
||||
public let additional: [String]
|
||||
|
||||
public var tags: [String] {
|
||||
[main] + additional
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
tags
|
||||
.map { "#\($0)" }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ public enum Timelines: Endpoint {
|
|||
case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool)
|
||||
case home(sinceId: String?, maxId: String?, minId: String?)
|
||||
case list(listId: String, sinceId: String?, maxId: String?, minId: String?)
|
||||
case hashtag(tag: String, maxId: String?)
|
||||
case hashtag(tag: String, additional: [String]?, maxId: String?)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
|
@ -14,11 +14,11 @@ public enum Timelines: Endpoint {
|
|||
return "timelines/home"
|
||||
case let .list(listId, _, _, _):
|
||||
return "timelines/list/\(listId)"
|
||||
case let .hashtag(tag, _):
|
||||
case let .hashtag(tag, _, _):
|
||||
return "timelines/tag/\(tag)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
switch self {
|
||||
case let .pub(sinceId, maxId, minId, local):
|
||||
|
@ -29,8 +29,11 @@ public enum Timelines: Endpoint {
|
|||
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||
case let .list(_, sinceId, maxId, mindId):
|
||||
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||
case let .hashtag(_, maxId):
|
||||
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
||||
case let .hashtag(_, additional, maxId):
|
||||
var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) ?? []
|
||||
params.append(contentsOf: (additional ?? [])
|
||||
.map { URLQueryItem(name: "any[]", value: $0) })
|
||||
return params
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,8 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
|
|||
|
||||
public enum TimelineFilter: Hashable, Equatable {
|
||||
case home, local, federated, trending
|
||||
case hashtag(tag: String, accountId: String?)
|
||||
case hashtag(tag: String, accountId: String?)
|
||||
case tagGroup(TagGroup)
|
||||
case list(list: Models.List)
|
||||
case remoteLocal(server: String, filter: RemoteTimelineFilter)
|
||||
case latest
|
||||
|
@ -72,6 +73,8 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||
return "Home"
|
||||
case let .hashtag(tag, _):
|
||||
return "#\(tag)"
|
||||
case let .tagGroup(group):
|
||||
return group.title
|
||||
case let .list(list):
|
||||
return list.title
|
||||
case let .remoteLocal(server, _):
|
||||
|
@ -93,6 +96,8 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||
return "timeline.home"
|
||||
case let .hashtag(tag, _):
|
||||
return "#\(tag)"
|
||||
case let .tagGroup(group):
|
||||
return LocalizedStringKey(group.title) // ?? not sure since this can't be localized.
|
||||
case let .list(list):
|
||||
return LocalizedStringKey(list.title)
|
||||
case let .remoteLocal(server, _):
|
||||
|
@ -142,8 +147,10 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||
if let accountId {
|
||||
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil)
|
||||
} else {
|
||||
return Timelines.hashtag(tag: tag, maxId: maxId)
|
||||
return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId)
|
||||
}
|
||||
case let .tagGroup(group):
|
||||
return Timelines.hashtag(tag: group.main, additional: group.additional, maxId: maxId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,6 +162,7 @@ extension TimelineFilter: Codable {
|
|||
case federated
|
||||
case trending
|
||||
case hashtag
|
||||
case tagGroup
|
||||
case list
|
||||
case remoteLocal
|
||||
case latest
|
||||
|
@ -180,6 +188,9 @@ extension TimelineFilter: Codable {
|
|||
tag: tag,
|
||||
accountId: accountId
|
||||
)
|
||||
case .tagGroup:
|
||||
let group = try container.decode(TagGroup.self, forKey: .tagGroup)
|
||||
self = .tagGroup(group)
|
||||
case .list:
|
||||
let list = try container.decode(
|
||||
Models.List.self,
|
||||
|
@ -221,6 +232,8 @@ extension TimelineFilter: Codable {
|
|||
var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag)
|
||||
try nestedContainer.encode(tag)
|
||||
try nestedContainer.encode(accountId)
|
||||
case let .tagGroup(group):
|
||||
try container.encode(group, forKey: .tagGroup)
|
||||
case let .list(list):
|
||||
try container.encode(list, forKey: .list)
|
||||
case let .remoteLocal(server, filter):
|
||||
|
|
|
@ -39,7 +39,9 @@ public struct TimelineView: View {
|
|||
ScrollViewReader { proxy in
|
||||
ZStack(alignment: .top) {
|
||||
List {
|
||||
if viewModel.tag == nil {
|
||||
if viewModel.tagGroup != nil {
|
||||
tagGroupHeaderView
|
||||
} else if viewModel.tag == nil {
|
||||
scrollToTopView
|
||||
} else {
|
||||
tagHeaderView
|
||||
|
@ -180,40 +182,64 @@ public struct TimelineView: View {
|
|||
@ViewBuilder
|
||||
private var tagHeaderView: some View {
|
||||
if let tag = viewModel.tag {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("#\(tag.name)")
|
||||
.font(.scaledHeadline)
|
||||
Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)")
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if tag.following {
|
||||
viewModel.tag = await account.unfollowTag(id: tag.name)
|
||||
} else {
|
||||
viewModel.tag = await account.followTag(id: tag.name)
|
||||
headerView {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("#\(tag.name)")
|
||||
.font(.scaledHeadline)
|
||||
Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)")
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if tag.following {
|
||||
viewModel.tag = await account.unfollowTag(id: tag.name)
|
||||
} else {
|
||||
viewModel.tag = await account.followTag(id: tag.name)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(tag.following ? "account.follow.following" : "account.follow.follow")
|
||||
}.buttonStyle(.bordered)
|
||||
}
|
||||
} label: {
|
||||
Text(tag.following ? "account.follow.following" : "account.follow.follow")
|
||||
}.buttonStyle(.bordered)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.init(top: 8,
|
||||
leading: .layoutPadding,
|
||||
bottom: 8,
|
||||
trailing: .layoutPadding))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tagGroupHeaderView: some View {
|
||||
if let group = viewModel.tagGroup {
|
||||
headerView {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(group.description)
|
||||
.font(.scaledHeadline)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func headerView(
|
||||
@ViewBuilder content: () -> some View
|
||||
) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
content()
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.init(top: 8,
|
||||
leading: .layoutPadding,
|
||||
bottom: 8,
|
||||
trailing: .layoutPadding))
|
||||
}
|
||||
|
||||
private var scrollToTopView: some View {
|
||||
HStack { EmptyView() }
|
||||
|
|
|
@ -40,6 +40,13 @@ class TimelineViewModel: ObservableObject {
|
|||
private var timelineTask: Task<Void, Never>?
|
||||
|
||||
@Published var tag: Tag?
|
||||
|
||||
var tagGroup: TagGroup? {
|
||||
if case let .tagGroup(group) = timeline {
|
||||
return group
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Internal source of truth for a timeline.
|
||||
private var datasource = TimelineDatasource()
|
||||
|
|
Loading…
Reference in a new issue