Skip to content

Kicked off Albums feature #346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: customized-master-v9.6.3-changes
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Nextcloud.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@
B5F5459E2DF1A883008137AB /* NCMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F322281649000D5EB0 /* NCMediaCell.swift */; };
B5F5459F2DF1C43B008137AB /* NCTrashListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78ACD4821903F850088454D /* NCTrashListCell.swift */; };
B5F545A12DF1C68F008137AB /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F545A02DF1C68F008137AB /* Shareable.swift */; };
C0A0D4852E1BD73700476BFF /* AlbumsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0D4842E1BD73700476BFF /* AlbumsViewController.swift */; };
C0A0D4892E1BDAC300476BFF /* AlbumsListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0D4882E1BDABD00476BFF /* AlbumsListScreen.swift */; };
C0A0D48B2E1BEBD400476BFF /* Albums+WebDAV.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0D48A2E1BEBC600476BFF /* Albums+WebDAV.swift */; };
C0A0D48D2E1CECF900476BFF /* AlbumsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0D48C2E1CECF200476BFF /* AlbumsListViewModel.swift */; };
D575039F27146F93008DC9DC /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; };
D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; };
F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */; };
Expand Down Expand Up @@ -1388,6 +1392,10 @@
B5F545A02DF1C68F008137AB /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = "<group>"; };
C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C0A0D4842E1BD73700476BFF /* AlbumsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumsViewController.swift; sourceTree = "<group>"; };
C0A0D4882E1BDABD00476BFF /* AlbumsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumsListScreen.swift; sourceTree = "<group>"; };
C0A0D48A2E1BEBC600476BFF /* Albums+WebDAV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Albums+WebDAV.swift"; sourceTree = "<group>"; };
C0A0D48C2E1CECF200476BFF /* AlbumsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumsListViewModel.swift; sourceTree = "<group>"; };
D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCActivityTableViewCell.swift; sourceTree = "<group>"; };
F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCViewerMedia+VisionKit.swift"; sourceTree = "<group>"; };
F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashSelectTabBar.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2306,6 +2314,25 @@
path = NextcloudUITests;
sourceTree = "<group>";
};
C0A0D4822E1BD6EA00476BFF /* Albums */ = {
isa = PBXGroup;
children = (
C0BFE58B2E1D2EAE00D945DB /* List */,
C0A0D48A2E1BEBC600476BFF /* Albums+WebDAV.swift */,
C0A0D4842E1BD73700476BFF /* AlbumsViewController.swift */,
);
path = Albums;
sourceTree = "<group>";
};
C0BFE58B2E1D2EAE00D945DB /* List */ = {
isa = PBXGroup;
children = (
C0A0D4882E1BDABD00476BFF /* AlbumsListScreen.swift */,
C0A0D48C2E1CECF200476BFF /* AlbumsListViewModel.swift */,
);
path = List;
sourceTree = "<group>";
};
F30A962A2A27A9C800D7BCFE /* Tests */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3492,6 +3519,7 @@
F7F67BAA1A24D27800EE80DA /* iOSClient */ = {
isa = PBXGroup;
children = (
C0A0D4822E1BD6EA00476BFF /* Albums */,
B5E2E73A2DAE89D700AB2EDD /* EmptyView */,
56A32E6F2CE4B75C0020EFF5 /* Analytics */,
B52FAE9B2DA8DED9001AB1BD /* NMC Custom Views */,
Expand Down Expand Up @@ -4635,6 +4663,7 @@
B5E2E7322DAE894C00AB2EDD /* NCSectionHeaderMenu.swift in Sources */,
F78C6FDE296D677300C952C3 /* NCContextMenu.swift in Sources */,
B5F079F42DF8557500EBD527 /* FileNameInputTextField.swift in Sources */,
C0A0D4892E1BDAC300476BFF /* AlbumsListScreen.swift in Sources */,
F7E402332BA89551007E5609 /* NCTrash+Networking.swift in Sources */,
B5F545962DF19556008137AB /* PasswordInputField.swift in Sources */,
F7E7AEA72BA32D0000512E52 /* NCCollectionViewUnifiedSearch.swift in Sources */,
Expand All @@ -4656,6 +4685,7 @@
F77DD6A82C5CC093009448FB /* NCSession.swift in Sources */,
F702F30825EE5D47008F8E80 /* NCPopupViewController.swift in Sources */,
B5E2E7282DAE880700AB2EDD /* NCCreateFormUploadVoiceNote.swift in Sources */,
C0A0D48D2E1CECF900476BFF /* AlbumsListViewModel.swift in Sources */,
B5F545972DF195D9008137AB /* AnalyticsHelper.swift in Sources */,
370D26AF248A3D7A00121797 /* NCCellProtocol.swift in Sources */,
F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */,
Expand Down Expand Up @@ -4753,6 +4783,7 @@
F7A846DE2BB01ACB0024816F /* NCTrashCellProtocol.swift in Sources */,
F799DF852C4B7E56003410B5 /* NCSectionHeader.swift in Sources */,
F78A10BF29322E8A008499B8 /* NCManageDatabase+Directory.swift in Sources */,
C0A0D4852E1BD73700476BFF /* AlbumsViewController.swift in Sources */,
F7743A122C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift in Sources */,
F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */,
F704B5E92430C0B800632F5F /* NCCreateFormUploadConflictCell.swift in Sources */,
Expand Down Expand Up @@ -4954,6 +4985,7 @@
F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */,
F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */,
F70968A424212C4E00ED60E5 /* NCLivePhoto.swift in Sources */,
C0A0D48B2E1BEBD400476BFF /* Albums+WebDAV.swift in Sources */,
F7C30DFA291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */,
F722133B2D40EF9D002F7438 /* NCFilesNavigationController.swift in Sources */,
F7BC288026663F85004D46C5 /* NCViewCertificateDetails.swift in Sources */,
Expand Down
239 changes: 239 additions & 0 deletions iOSClient/Albums/Albums+WebDAV.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
//
// Albums+WebDAV.swift
// Nextcloud
//
// Created by A200118228 on 07/07/25.
// Copyright © 2025 Marino Faggiana. All rights reserved.
//

import NextcloudKit
import Alamofire
import SwiftyJSON
import SwiftyXMLParser

public struct Album {
let name: String
let lastPhotoId: String?
let itemCount: Int?
let location: String?
let dateRange: String?
let collaborators: String?
}

public extension NextcloudKit {

func fetchAllAlbums(
for account: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
completion: @escaping (Result<[Album], Error>) -> Void
) {

let session = NCSession.shared.getSession(account: account)

//options.contentType = "application/xml"

let urlPath = session.urlBase + "/remote.php/dav/photos/" + session.user + "/albums/"

guard let nkSession = nkCommonInstance.getSession(account: account),
let url = urlPath.encodedToUrl,
let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else {
return options.queue.async { completion(.failure(NKError.urlError)) }
}

let method = HTTPMethod(rawValue: "PROPFIND")

let propfindXML = """
<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns" xmlns:ocs="http://open-collaboration-services.org/ns">
<d:prop>
<nc:last-photo />
<nc:nbItems />
<nc:location />
<nc:dateRange />
<nc:collaborators />
</d:prop>
</d:propfind>
"""

var urlRequest: URLRequest
do {
try urlRequest = URLRequest(url: url, method: method, headers: headers)
urlRequest.httpBody = propfindXML.data(using: .utf8)
urlRequest.timeoutInterval = options.timeout
} catch {
return options.queue.async { completion(.failure(NKError(error: error))) }
}

nkSession.sessionData.request(
urlRequest,
//interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)
)
.validate(statusCode: 200..<300)
.onURLSessionTaskCreation { task in
task.taskDescription = options.taskDescription
taskHandler(task)
}
.response(queue: self.nkCommonInstance.backgroundQueue) { response in

if self.nkCommonInstance.levelLog > 0 {
debugPrint(response)
}

let statusCode = response.response?.statusCode

// Explicit 404 check
if statusCode == 404 {
let error = NKError.success
return options.queue.async {
completion(.success([]))
}
}

switch response.result {

case .failure(let error):
let error = NKError(error: error, afResponse: response, responseData: response.data)
options.queue.async { completion(.failure(error)) }

case .success:

guard let data = response.data else {
return options.queue.async {
completion(.failure(NKError.invalidData))
}
}

let albums = self.parseAlbumsXML(data: data)
options.queue.async {
completion(.success(albums))
}
}
}
}

private func parseAlbumsXML(data: Data) -> [Album] {

let xml = XML.parse(data)
var albums: [Album] = []

let elements = xml["d:multistatus", "d:response"]

for element in elements {

let href = element["d:href"].element?.text ?? ""

let prop = element["d:propstat"]["d:prop"]

let lastPhoto = prop["nc:last-photo"].element?.text
let nbItems = prop["nc:nbItems"].element?.text.flatMap { Int($0) }
let location = prop["nc:location"].element?.text
let dateRange = prop["nc:dateRange"].element?.text
let collaborators = prop["nc:collaborators"].element?.text

// Optionally skip entries with 404 status
let status = element["d:propstat"]["d:status"].element?.text ?? ""
if status.contains("200") {
let album = Album(
name: href,
lastPhotoId: nil,
itemCount: nil,
location: nil,
dateRange: nil,
collaborators: nil
)
albums.append(album)
}
}

return albums

// return [
// Album(
// name: "Sample Album",
// lastPhotoId: nil,
// itemCount: nil,
// location: nil,
// dateRange: nil,
// collaborators: nil
// )
// ]
}

func createNewAlbum(
for account: String,
albumName: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
completion: @escaping (Result<Bool, Error>) -> Void
) {

let session = NCSession.shared.getSession(account: account)

//options.contentType = "application/xml"

let urlPath = session.urlBase + "/remote.php/dav/photos/" + session.user + "/albums/\(albumName)/"

guard let nkSession = nkCommonInstance.getSession(account: account),
let url = urlPath.encodedToUrl,
let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else {
return options.queue.async { completion(.failure(NKError.urlError)) }
}

let method = HTTPMethod(rawValue: "MKCOL")

var urlRequest: URLRequest
do {
try urlRequest = URLRequest(url: url, method: method, headers: headers)
urlRequest.timeoutInterval = options.timeout
} catch {
return options.queue.async { completion(.failure(NKError(error: error))) }
}

nkSession.sessionData.request(
urlRequest,
//interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)
)
// .validate(statusCode: 200..<300)
.onURLSessionTaskCreation { task in
task.taskDescription = options.taskDescription
taskHandler(task)
}
.response(queue: self.nkCommonInstance.backgroundQueue) { response in

if self.nkCommonInstance.levelLog > 0 {
debugPrint(response)
}

let statusCode = response.response?.statusCode

// Explicit 405 check
if statusCode == 405 {
let error = NKError(errorCode: 405, errorDescription: "Album already exists!", responseData: nil)
return options.queue.async {
completion(.failure(error))
}
}

switch response.result {

case .failure(let error):
let error = NKError(error: error, afResponse: response, responseData: response.data)
options.queue.async { completion(.failure(error)) }

case .success:

guard let data = response.data else {
return options.queue.async {
completion(.failure(NKError.invalidData))
}
}

let albums = self.parseAlbumsXML(data: data)
options.queue.async {
completion(.success(true))
}
}
}
}
}
37 changes: 37 additions & 0 deletions iOSClient/Albums/AlbumsViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// AlbumsController.swift
// Nextcloud
//
// Created by A200118228 on 07/07/25.
// Copyright © 2025 Marino Faggiana. All rights reserved.
//

import UIKit
import SwiftUI

class AlbumsViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let appDelegate = UIApplication.shared.delegate as! AppDelegate

let viewModel = AlbumsListViewModel(account: appDelegate.account)
let albumsView = AlbumsListScreen(viewModel: viewModel)

let hostingController = UIHostingController(rootView: albumsView)

addChild(hostingController)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingController.view)

NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])

hostingController.didMove(toParent: self)
}
}
Loading