> getRecordedErrorsByProgressName() {
41 | return unmodifiableMap(recordedErrorsByProgressName);
42 | }
43 |
44 | public void reset() {
45 | recordedErrorsByProgressName.clear();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/UploadStateManagerImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import net.yudichev.jiotty.common.varstore.VarStore;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 |
7 | import javax.inject.Inject;
8 | import java.util.concurrent.locks.Lock;
9 | import java.util.concurrent.locks.ReentrantLock;
10 |
11 | import static com.google.common.base.Preconditions.checkNotNull;
12 | import static java.lang.System.identityHashCode;
13 | import static net.yudichev.jiotty.common.lang.Locks.inLock;
14 |
15 | final class UploadStateManagerImpl implements UploadStateManager {
16 | private static final Logger logger = LoggerFactory.getLogger(UploadStateManagerImpl.class);
17 |
18 | private static final String VAR_STORE_KEY = "photosUploader";
19 | private final VarStore varStore;
20 | private final Lock lock = new ReentrantLock();
21 | private UploadState uploadState;
22 |
23 | @Inject
24 | UploadStateManagerImpl(VarStore varStore) {
25 | this.varStore = checkNotNull(varStore);
26 | uploadState = varStore.readValue(UploadState.class, VAR_STORE_KEY).orElseGet(() -> UploadState.builder().build());
27 | }
28 |
29 | @Override
30 | public UploadState get() {
31 | return inLock(lock, () -> uploadState);
32 | }
33 |
34 | @Override
35 | public void save(UploadState uploadState) {
36 | inLock(lock, () -> {
37 | this.uploadState = uploadState;
38 | varStore.saveValue(VAR_STORE_KEY, uploadState);
39 | logger.debug("Saved state {} with {} item(s)", identityHashCode(uploadState), uploadState.uploadedMediaItemIdByAbsolutePath().size());
40 | });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/OperatingSystemDetection.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 |
4 | import java.util.Locale;
5 |
6 | /**
7 | * helper class to check the operating system this Java VM runs in
8 | *
9 | * please keep the notes below as a pseudo-license
10 | *
11 | * http://stackoverflow.com/questions/228477/how-do-i-programmatically-determine-operating-system-in-java
12 | * compare to http://svn.terracotta.org/svn/tc/dso/tags/2.6.4/code/base/common/src/com/tc/util/runtime/Os.java
13 | * http://www.docjar.com/html/api/org/apache/commons/lang/SystemUtils.java.html
14 | */
15 | public final class OperatingSystemDetection {
16 | // cached result of OS detection
17 | private static OSType detectedOS;
18 |
19 | /**
20 | * detect the operating system from the os.name System property and cache
21 | * the result
22 | *
23 | * @return the operating system detected
24 | */
25 | public static OSType getOperatingSystemType() {
26 | if (detectedOS == null) {
27 | var OS = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
28 | if ((OS.contains("mac")) || (OS.contains("darwin"))) {
29 | detectedOS = OSType.MacOS;
30 | } else if (OS.contains("win")) {
31 | detectedOS = OSType.Windows;
32 | } else if (OS.contains("nux")) {
33 | detectedOS = OSType.Linux;
34 | } else {
35 | detectedOS = OSType.Other;
36 | }
37 | }
38 | return detectedOS;
39 | }
40 |
41 | /**
42 | * types of Operating Systems
43 | */
44 | public enum OSType {
45 | Windows, MacOS, Linux, Other
46 | }
47 | }
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/AddToAlbumWhileCreatingStrategy.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import net.yudichev.jiotty.connector.google.photos.GooglePhotosAlbum;
4 |
5 | import java.nio.file.Path;
6 | import java.util.List;
7 | import java.util.Optional;
8 | import java.util.concurrent.CompletableFuture;
9 | import java.util.function.BiFunction;
10 | import java.util.function.Function;
11 |
12 | import static com.google.common.collect.Lists.partition;
13 | import static net.yudichev.googlephotosupload.core.GooglePhotosUploaderImpl.GOOGLE_PHOTOS_API_BATCH_SIZE;
14 | import static net.yudichev.jiotty.common.lang.CompletableFutures.toFutureOfList;
15 |
16 | final class AddToAlbumWhileCreatingStrategy implements AddToAlbumStrategy {
17 | @Override
18 | public CompletableFuture addToAlbum(CompletableFuture> createMediaDataResultsFuture,
19 | Optional googlePhotosAlbum,
20 | ProgressStatus fileProgressStatus,
21 | BiFunction, List, CompletableFuture>> createMediaItems,
22 | Function itemStateRetriever) {
23 | return createMediaDataResultsFuture
24 | .thenCompose(createMediaDataResults -> partition(createMediaDataResults, GOOGLE_PHOTOS_API_BATCH_SIZE).stream()
25 | .map(pathStates -> createMediaItems.apply(googlePhotosAlbum.map(GooglePhotosAlbum::getId), pathStates))
26 | .collect(toFutureOfList())
27 | .thenApply(voids -> null));
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/resources/FailureLog.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/AboutDialogFxController.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import javafx.event.ActionEvent;
4 | import javafx.scene.control.Hyperlink;
5 | import javafx.scene.control.TextField;
6 | import javafx.scene.layout.Pane;
7 |
8 | import javax.inject.Inject;
9 | import javax.inject.Provider;
10 |
11 | import static com.google.common.base.Preconditions.checkNotNull;
12 | import static net.yudichev.googlephotosupload.core.AppGlobals.APP_TITLE;
13 | import static net.yudichev.googlephotosupload.core.BuildVersion.buildVersion;
14 |
15 | public final class AboutDialogFxController {
16 | private final FxmlContainerFactory fxmlContainerFactory;
17 | private final Provider javafxApplicationResourcesProvider;
18 | public Hyperlink titleHyperlink;
19 | public Pane supportPane;
20 | public TextField versionLabel;
21 |
22 | @Inject
23 | public AboutDialogFxController(FxmlContainerFactory fxmlContainerFactory,
24 | Provider javafxApplicationResourcesProvider) {
25 | this.fxmlContainerFactory = checkNotNull(fxmlContainerFactory);
26 | this.javafxApplicationResourcesProvider = checkNotNull(javafxApplicationResourcesProvider);
27 | }
28 |
29 | public void initialize() {
30 | titleHyperlink.setText(APP_TITLE);
31 | versionLabel.setText(buildVersion());
32 | supportPane.getChildren().add(fxmlContainerFactory.create("SupportPane.fxml").root());
33 | }
34 |
35 | public void onTitleHyperlinkLinkAction(ActionEvent actionEvent) {
36 | javafxApplicationResourcesProvider.get().hostServices().showDocument("http://jiotty-photos-uploader.yudichev.net");
37 | actionEvent.consume();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/test/java/net/yudichev/googlephotosupload/core/OptionalMatchers.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * -\-\-
3 | * hamcrest-optional
4 | * --
5 | * Copyright (C) 2016 Spotify AB
6 | * --
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * -/-/-
19 | */
20 |
21 | package net.yudichev.googlephotosupload.core;
22 |
23 | import org.hamcrest.Matcher;
24 |
25 | import java.util.Optional;
26 |
27 | import static org.hamcrest.CoreMatchers.anything;
28 |
29 | public final class OptionalMatchers {
30 |
31 | private OptionalMatchers() {
32 | }
33 |
34 | /**
35 | * Creates a Matcher that matches empty Optionals.
36 | */
37 | public static Matcher> emptyOptional() {
38 | return new EmptyOptional<>();
39 | }
40 |
41 | /**
42 | * Creates a Matcher that matches any Optional with a value.
43 | */
44 | public static Matcher> optionalWithValue() {
45 | return optionalWithValue(anything());
46 | }
47 |
48 | /**
49 | * Creates a Matcher that matches an Optional with a value that matches the given Matcher.
50 | */
51 | public static Matcher> optionalWithValue(Matcher matcher) {
52 | return new PresentOptional<>(matcher);
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/src/main/resources/AboutDialog.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/SingleInstanceCheck.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | import java.io.IOException;
7 | import java.nio.channels.FileChannel;
8 | import java.nio.channels.FileLock;
9 |
10 | import static java.nio.file.StandardOpenOption.CREATE;
11 | import static java.nio.file.StandardOpenOption.WRITE;
12 | import static net.yudichev.googlephotosupload.core.AppGlobals.APP_SETTINGS_DIR;
13 | import static net.yudichev.googlephotosupload.core.ResourceBundleModule.RESOURCE_BUNDLE;
14 | import static net.yudichev.googlephotosupload.ui.FatalStartupError.showFatalStartupError;
15 |
16 | final class SingleInstanceCheck {
17 | private static final Logger logger = LoggerFactory.getLogger(SingleInstanceCheck.class);
18 | // keeps the reference to the lock so that it's not garbage collected
19 | @SuppressWarnings({"FieldCanBeLocal", "StaticVariableMayNotBeInitialized"})
20 | private static FileLock LOCK;
21 |
22 | public static boolean otherInstanceRunning() {
23 | var lockFile = APP_SETTINGS_DIR.resolve("instance.lock");
24 | try {
25 | LOCK = FileChannel.open(lockFile, CREATE, WRITE).tryLock();
26 | } catch (IOException | RuntimeException e) {
27 | logger.error("Exception trying to lock the instance file", e);
28 | // should be pretty rare; best we can do is assume the instance is not running
29 | return false;
30 | }
31 | if (LOCK == null) {
32 | showFatalStartupError(RESOURCE_BUNDLE.getString("singleInstanceCheckDialogMessage"));
33 | return true;
34 | }
35 | //noinspection StaticVariableUsedBeforeInitialization it's not
36 | logger.debug("Acquired instance lock {} on {}", LOCK, lockFile);
37 | return false;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/DialogImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import com.google.inject.assistedinject.Assisted;
4 | import javafx.scene.Scene;
5 | import javafx.scene.image.Image;
6 | import javafx.stage.Stage;
7 |
8 | import javax.inject.Inject;
9 | import javax.inject.Provider;
10 | import java.util.function.Consumer;
11 |
12 | final class DialogImpl implements Dialog {
13 | private final Object fxController;
14 | private final Stage dialog;
15 |
16 | @Inject
17 | DialogImpl(Provider javafxApplicationResourcesProvider,
18 | FxmlContainerFactory fxmlContainerFactory,
19 | @Assisted("title") String title,
20 | @Assisted("fxmlPath") String fxmlPath,
21 | @Assisted Consumer customizer) {
22 | var preferencesDialogFxContainer = fxmlContainerFactory.create(fxmlPath);
23 | fxController = preferencesDialogFxContainer.controller();
24 | var primaryStage = javafxApplicationResourcesProvider.get().primaryStage();
25 | dialog = new Stage();
26 | dialog.getIcons().add(new Image(getClass().getResourceAsStream("/Icon1024.png")));
27 | dialog.setTitle(title);
28 | dialog.setScene(new Scene(preferencesDialogFxContainer.root()));
29 | dialog.initOwner(primaryStage);
30 | customizer.accept(dialog);
31 | }
32 |
33 | @Override
34 | public void show() {
35 | dialog.show();
36 | dialog.toFront();
37 | }
38 |
39 | @SuppressWarnings("unchecked")
40 | @Override
41 | public T controller() {
42 | return (T) fxController;
43 | }
44 |
45 | @Override
46 | public void sizeToScene() {
47 | dialog.sizeToScene();
48 | }
49 |
50 | @Override
51 | public void close() {
52 | dialog.close();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/FatalUserCorrectableRemoteApiExceptionHandlerImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import io.grpc.Status;
4 | import io.grpc.StatusRuntimeException;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import java.util.function.Predicate;
9 |
10 | import static com.google.common.base.Throwables.getCausalChain;
11 | import static java.lang.Boolean.FALSE;
12 | import static java.lang.Boolean.TRUE;
13 |
14 | final class FatalUserCorrectableRemoteApiExceptionHandlerImpl implements FatalUserCorrectableRemoteApiExceptionHandler {
15 | private static final Logger logger = LoggerFactory.getLogger(FatalUserCorrectableRemoteApiExceptionHandlerImpl.class);
16 | private static final Predicate PREDICATE = (
17 | // TODO this is a special case, if ever https://github.com/google/java-photoslibrary/issues/29 is fixed,
18 | // this workaround should be removed
19 | (Predicate) e -> e instanceof IllegalArgumentException && e.getMessage().contains("failed to finalize or get the result"))
20 |
21 | // https://github.com/ylexus/jiotty-photos-uploader/issues/14: this covers issues like "No permissions to add this media item to the album"
22 | .or(e -> e instanceof StatusRuntimeException && ((StatusRuntimeException) e).getStatus().getCode() == Status.Code.INVALID_ARGUMENT);
23 |
24 | @Override
25 | public boolean handle(String operationName, Throwable exception) {
26 | return getCausalChain(exception).stream()
27 | .filter(PREDICATE)
28 | .findFirst()
29 | .map(e -> {
30 | logger.debug("Fatal user correctable error while performing '{}'", operationName, e);
31 | return TRUE;
32 | })
33 | .orElse(FALSE);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/ProgressStatusBarImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import com.google.inject.assistedinject.Assisted;
4 | import javafx.scene.Node;
5 | import javafx.scene.Parent;
6 | import net.yudichev.googlephotosupload.core.KeyedError;
7 |
8 | import javax.inject.Inject;
9 | import java.util.Collection;
10 | import java.util.Optional;
11 | import java.util.concurrent.atomic.AtomicBoolean;
12 |
13 | final class ProgressStatusBarImpl implements ProgressStatusBar {
14 | private final Parent root;
15 | private final ProgressBoxFxController controller;
16 | private final AtomicBoolean hasFailures = new AtomicBoolean();
17 |
18 | @Inject
19 | ProgressStatusBarImpl(FxmlContainerFactory fxmlContainerFactory,
20 | @Assisted String name,
21 | @Assisted Optional totalCount) {
22 | var fxmlContainer = fxmlContainerFactory.create("ProgressBox.fxml");
23 | controller = fxmlContainer.controller();
24 | controller.init(name, totalCount);
25 | root = fxmlContainer.root();
26 | }
27 |
28 | @Override
29 | public void updateSuccess(int newValue) {
30 | controller.updateSuccess(newValue);
31 | }
32 |
33 | @Override
34 | public void addFailures(Collection failures) {
35 | hasFailures.set(true);
36 | controller.addFailures(failures);
37 | }
38 |
39 | @Override
40 | public void completed(boolean success) {
41 | controller.done(success);
42 | }
43 |
44 | @Override
45 | public void onBackoffDelay(long backoffDelayMs) {
46 | controller.onBackoffDelay(backoffDelayMs);
47 | }
48 |
49 | @Override
50 | public Node node() {
51 | return root;
52 | }
53 |
54 | @Override
55 | public boolean hasFailures() {
56 | return hasFailures.get();
57 | }
58 |
59 | @Override
60 | public void close() {
61 | controller.close();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/cli/CliStarter.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.cli;
2 |
3 | import net.yudichev.googlephotosupload.core.Uploader;
4 | import net.yudichev.jiotty.common.app.ApplicationLifecycleControl;
5 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponent;
6 | import org.apache.commons.cli.CommandLine;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | import javax.inject.Inject;
11 | import java.nio.file.Path;
12 | import java.nio.file.Paths;
13 | import java.util.ResourceBundle;
14 |
15 | import static com.google.common.base.Preconditions.checkNotNull;
16 | import static net.yudichev.jiotty.common.lang.CompletableFutures.logErrorOnFailure;
17 |
18 | final class CliStarter extends BaseLifecycleComponent {
19 | private static final Logger logger = LoggerFactory.getLogger(CliStarter.class);
20 | private final Path rootDir;
21 | private final Uploader uploader;
22 | private final ApplicationLifecycleControl applicationLifecycleControl;
23 | private final ResourceBundle resourceBundle;
24 | private final boolean resume;
25 |
26 | @Inject
27 | CliStarter(CommandLine commandLine,
28 | Uploader uploader,
29 | ApplicationLifecycleControl applicationLifecycleControl,
30 | ResourceBundle resourceBundle) {
31 | rootDir = Paths.get(commandLine.getOptionValue('r'));
32 | resume = !commandLine.hasOption('n');
33 | this.uploader = checkNotNull(uploader);
34 | this.applicationLifecycleControl = checkNotNull(applicationLifecycleControl);
35 | this.resourceBundle = checkNotNull(resourceBundle);
36 | }
37 |
38 | @Override
39 | protected void doStart() {
40 | logger.info(resourceBundle.getString("googleStorageWarning"));
41 | uploader.upload(rootDir, resume)
42 | .whenComplete(logErrorOnFailure(logger, "Failed"))
43 | .whenComplete((ignored1, ignored2) -> applicationLifecycleControl.initiateShutdown());
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/RestarterImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import com.google.inject.BindingAnnotation;
4 | import net.yudichev.jiotty.common.app.ApplicationLifecycleControl;
5 |
6 | import javax.inject.Inject;
7 | import java.lang.annotation.Retention;
8 | import java.lang.annotation.Target;
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 |
12 | import static com.google.common.base.Preconditions.checkNotNull;
13 | import static com.google.common.io.MoreFiles.deleteRecursively;
14 | import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
15 | import static java.lang.annotation.ElementType.*;
16 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
17 | import static net.yudichev.jiotty.common.lang.MoreThrowables.asUnchecked;
18 |
19 | final class RestarterImpl implements Restarter {
20 | private final Path googleAuthRootDir;
21 | private final ApplicationLifecycleControl applicationLifecycleControl;
22 | private final Uploader uploader;
23 |
24 | @Inject
25 | RestarterImpl(@GoogleAuthRootDir Path googleAuthRootDir,
26 | ApplicationLifecycleControl applicationLifecycleControl,
27 | Uploader uploader) {
28 | this.googleAuthRootDir = checkNotNull(googleAuthRootDir);
29 | this.applicationLifecycleControl = checkNotNull(applicationLifecycleControl);
30 | this.uploader = checkNotNull(uploader);
31 | }
32 |
33 | @Override
34 | public void initiateLogoutAndRestart() {
35 | if (Files.exists(googleAuthRootDir)) {
36 | asUnchecked(() -> deleteRecursively(googleAuthRootDir, ALLOW_INSECURE));
37 | uploader.forgetUploadState();
38 | }
39 | applicationLifecycleControl.initiateRestart();
40 | }
41 |
42 | @Override
43 | public void initiateRestart() {
44 | applicationLifecycleControl.initiateRestart();
45 | }
46 |
47 | @BindingAnnotation
48 | @Target({FIELD, PARAMETER, METHOD})
49 | @Retention(RUNTIME)
50 | @interface GoogleAuthRootDir {
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/resources/UploaderStrategyChoicePanel.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/test/java/net/yudichev/googlephotosupload/core/PresentOptional.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * -\-\-
3 | * hamcrest-optional
4 | * --
5 | * Copyright (C) 2016 Spotify AB
6 | * --
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * -/-/-
19 | */
20 |
21 | package net.yudichev.googlephotosupload.core;
22 |
23 | import org.hamcrest.Description;
24 | import org.hamcrest.Matcher;
25 | import org.hamcrest.TypeSafeDiagnosingMatcher;
26 |
27 | import java.util.Optional;
28 |
29 | class PresentOptional extends TypeSafeDiagnosingMatcher> {
30 |
31 | private final Matcher matcher;
32 |
33 | PresentOptional(Matcher matcher) {
34 | this.matcher = matcher;
35 | }
36 |
37 | @Override
38 | protected boolean matchesSafely(Optional extends T> item,
39 | Description mismatchDescription) {
40 | if (item.isPresent()) {
41 | if (matcher.matches(item.get())) {
42 | return true;
43 | } else {
44 | mismatchDescription.appendText("was an Optional whose value ");
45 | matcher.describeMismatch(item.get(), mismatchDescription);
46 | return false;
47 | }
48 | } else {
49 | mismatchDescription.appendText("was not present");
50 | return false;
51 | }
52 | }
53 |
54 | @Override
55 | public void describeTo(Description description) {
56 | description
57 | .appendText("an Optional with a value that ")
58 | .appendDescriptionOf(matcher);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/test/java/net/yudichev/googlephotosupload/core/IntegrationTestUploadStarterModule.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponentModule;
4 | import org.apache.commons.cli.CommandLine;
5 |
6 | import java.util.function.Function;
7 |
8 | import static com.google.common.base.Preconditions.checkNotNull;
9 | import static net.yudichev.googlephotosupload.core.AddToAlbumMethod.AFTER_CREATING_ITEMS_SORTED;
10 |
11 | public final class IntegrationTestUploadStarterModule extends BaseLifecycleComponentModule {
12 | private static Preferences preferences;
13 |
14 | static {
15 | setDefaultPreferences();
16 | }
17 |
18 | private final CommandLine commandLine;
19 | private final ProgressStatusFactory progressStatusFactory;
20 |
21 | public IntegrationTestUploadStarterModule(CommandLine commandLine, ProgressStatusFactory progressStatusFactory) {
22 | this.commandLine = checkNotNull(commandLine);
23 | this.progressStatusFactory = checkNotNull(progressStatusFactory);
24 | }
25 |
26 | public static void setDefaultPreferences() {
27 | preferences = Preferences.builder().setAddToAlbumStrategy(AFTER_CREATING_ITEMS_SORTED).build();
28 | }
29 |
30 | public static void modifyPreferences(Function modifier) {
31 | preferences = modifier.apply(preferences);
32 | }
33 |
34 | @Override
35 | protected void configure() {
36 | bind(CommandLine.class).toInstance(commandLine);
37 | boundLifecycleComponent(IntegrationTestUploadStarter.class);
38 |
39 | bind(PreferencesManager.class).toInstance(new PreferencesManager() {
40 | @Override
41 | public void update(Function updater) {
42 | }
43 |
44 | @Override
45 | public Preferences get() {
46 | return preferences;
47 | }
48 | });
49 | expose(PreferencesManager.class);
50 |
51 | bind(ProgressStatusFactory.class).toInstance(progressStatusFactory);
52 | expose(ProgressStatusFactory.class);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/UploaderStrategyChoicePanelControllerImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import javafx.event.ActionEvent;
4 | import javafx.scene.Node;
5 | import javafx.scene.control.RadioButton;
6 | import javafx.scene.layout.VBox;
7 | import net.yudichev.googlephotosupload.core.AddToAlbumMethod;
8 | import net.yudichev.jiotty.common.lang.Closeable;
9 | import net.yudichev.jiotty.common.lang.CompositeException;
10 |
11 | import java.util.List;
12 | import java.util.concurrent.CopyOnWriteArrayList;
13 | import java.util.function.Consumer;
14 |
15 | import static net.yudichev.googlephotosupload.core.AddToAlbumMethod.AFTER_CREATING_ITEMS_SORTED;
16 | import static net.yudichev.googlephotosupload.core.AddToAlbumMethod.WHILE_CREATING_ITEMS;
17 |
18 | public final class UploaderStrategyChoicePanelControllerImpl implements UploaderStrategyChoicePanelController {
19 | private final List> listeners = new CopyOnWriteArrayList<>();
20 | public RadioButton addToAlbumWhileCreatingRadioButton;
21 | public RadioButton addAfterCreatingItemsRadioButton;
22 | public VBox root;
23 |
24 | @Override
25 | public Node getRoot() {
26 | return root;
27 | }
28 |
29 | @Override
30 | public void setSelection(AddToAlbumMethod addToAlbumMethod) {
31 | addAfterCreatingItemsRadioButton.setSelected(addToAlbumMethod == AFTER_CREATING_ITEMS_SORTED);
32 | addToAlbumWhileCreatingRadioButton.setSelected(addToAlbumMethod == WHILE_CREATING_ITEMS);
33 | }
34 |
35 | public void onRadioButtonSelectionChange(ActionEvent actionEvent) {
36 | CompositeException.runForAll(listeners, listener -> {
37 | var method = addAfterCreatingItemsRadioButton.isSelected() ? AFTER_CREATING_ITEMS_SORTED : WHILE_CREATING_ITEMS;
38 | listener.accept(method);
39 | });
40 | actionEvent.consume();
41 | }
42 |
43 | @Override
44 | public Closeable addSelectionChangeListener(Consumer selectionChangeHandler) {
45 | listeners.add(selectionChangeHandler);
46 | return () -> listeners.remove(selectionChangeHandler);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/resources/FolderSelector.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/DefaultPlatformSpecificMenu.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import javafx.event.ActionEvent;
4 | import javafx.event.EventHandler;
5 | import javafx.scene.control.Menu;
6 | import javafx.scene.control.MenuBar;
7 | import javafx.scene.control.MenuItem;
8 | import javafx.scene.control.SeparatorMenuItem;
9 |
10 | import javax.inject.Inject;
11 | import java.util.ResourceBundle;
12 |
13 | import static com.google.common.base.Preconditions.checkNotNull;
14 |
15 | final class DefaultPlatformSpecificMenu implements PlatformSpecificMenu {
16 | private final ResourceBundle resourceBundle;
17 | private MenuItem exitMenuItem;
18 | private MenuItem preferencesMenuItem;
19 | private MenuItem aboutMenuItem;
20 |
21 | @Inject
22 | DefaultPlatformSpecificMenu(ResourceBundle resourceBundle) {
23 | this.resourceBundle = checkNotNull(resourceBundle);
24 | }
25 |
26 | @Override
27 | public void initialize(MenuBar menuBar) {
28 | var fileMenu = new Menu(resourceBundle.getString("menuItemDefaultFile"));
29 | var fileMenuItems = fileMenu.getItems();
30 |
31 | aboutMenuItem = new MenuItem(resourceBundle.getString("menuItemDefaultAbout"));
32 | fileMenuItems.add(aboutMenuItem);
33 |
34 | preferencesMenuItem = new MenuItem(resourceBundle.getString("menuItemDefaultSettings"));
35 | fileMenuItems.add(preferencesMenuItem);
36 |
37 | fileMenuItems.add(new SeparatorMenuItem());
38 | exitMenuItem = new MenuItem(resourceBundle.getString("menuItemDefaultExit"));
39 |
40 | fileMenuItems.add(exitMenuItem);
41 | menuBar.getMenus().add(0, fileMenu);
42 | }
43 |
44 | @Override
45 | public void setOnExitAction(EventHandler onExitEventHandler) {
46 | exitMenuItem.setOnAction(onExitEventHandler);
47 | }
48 |
49 | @Override
50 | public void setOnPreferencesAction(EventHandler onPreferencesEventHandler) {
51 | preferencesMenuItem.setOnAction(onPreferencesEventHandler);
52 | }
53 |
54 | @Override
55 | public void setOnAboutAction(EventHandler onAboutAction) {
56 | aboutMenuItem.setOnAction(onAboutAction);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/BackpressuredExecutorServiceProvider.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import com.google.common.util.concurrent.ThreadFactoryBuilder;
4 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponent;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import javax.inject.Provider;
9 | import java.util.concurrent.ExecutorService;
10 | import java.util.concurrent.LinkedBlockingQueue;
11 | import java.util.concurrent.ThreadPoolExecutor;
12 |
13 | import static com.google.common.base.Preconditions.checkState;
14 | import static com.google.common.util.concurrent.MoreExecutors.shutdownAndAwaitTermination;
15 | import static java.util.concurrent.TimeUnit.MILLISECONDS;
16 | import static java.util.concurrent.TimeUnit.SECONDS;
17 |
18 | final class BackpressuredExecutorServiceProvider extends BaseLifecycleComponent implements Provider {
19 | private static final Logger logger = LoggerFactory.getLogger(BackpressuredExecutorServiceProvider.class);
20 | private ThreadPoolExecutor executor;
21 |
22 | @Override
23 | public ExecutorService get() {
24 | return whenStartedAndNotLifecycling(() -> executor);
25 | }
26 |
27 | private static void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
28 | checkState(!executor.isShutdown(), "Executor shut down: %s", executor);
29 | task.run();
30 | }
31 |
32 | @Override
33 | protected void doStop() {
34 | if (!shutdownAndAwaitTermination(executor, 30, SECONDS)) {
35 | logger.warn("Failed to shutdown upload thread pool in 3 seconds!");
36 | }
37 | //noinspection AssignmentToNull
38 | executor = null;
39 | }
40 |
41 | @Override
42 | protected void doStart() {
43 | executor = new ThreadPoolExecutor(
44 | 1,
45 | 1,
46 | 0L, MILLISECONDS,
47 | new LinkedBlockingQueue<>(2),
48 | new ThreadFactoryBuilder()
49 | .setNameFormat("upload-pool-%s")
50 | .setDaemon(true)
51 | .build(),
52 | BackpressuredExecutorServiceProvider::rejectedExecution);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/cli/CliMain.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.cli;
2 |
3 | import net.yudichev.googlephotosupload.core.DependenciesModule;
4 | import net.yudichev.googlephotosupload.core.ResourceBundleModule;
5 | import net.yudichev.googlephotosupload.core.UploadPhotosModule;
6 | import net.yudichev.jiotty.common.app.Application;
7 | import org.apache.commons.cli.CommandLineParser;
8 | import org.apache.commons.cli.DefaultParser;
9 | import org.apache.commons.cli.HelpFormatter;
10 | import org.apache.commons.cli.ParseException;
11 | import org.apache.logging.log4j.LogManager;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 |
15 | import java.io.PrintWriter;
16 | import java.io.StringWriter;
17 |
18 | public final class CliMain {
19 | public static void main(String[] args) {
20 | CommandLineParser parser = new DefaultParser();
21 | try {
22 | var commandLine = parser.parse(CliOptions.OPTIONS, args);
23 | Application.builder()
24 | .addModule(() -> DependenciesModule.builder().build())
25 | .addModule(() -> new UploadPhotosModule(1000))
26 | .addModule(ResourceBundleModule::new)
27 | .addModule(() -> new CliModule(commandLine))
28 | .build()
29 | .run();
30 | } catch (ParseException e) {
31 | var logger = LoggerFactory.getLogger(CliMain.class);
32 | logger.error(e.getMessage());
33 | printHelp(logger);
34 | }
35 | LogManager.shutdown();
36 | }
37 |
38 | private static void printHelp(Logger logger) {
39 | var helpFormatter = new HelpFormatter();
40 | helpFormatter.setWidth(100);
41 | var sw = new StringWriter();
42 | var pw = new PrintWriter(sw);
43 | helpFormatter.printHelp(pw, helpFormatter.getWidth(),
44 | "Jiotty Photos Uploader",
45 | null,
46 | CliOptions.OPTIONS,
47 | helpFormatter.getLeftPadding(),
48 | helpFormatter.getDescPadding(),
49 | null,
50 | false);
51 | pw.flush();
52 | var help = sw.toString();
53 | logger.info(help);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/java/net/yudichev/googlephotosupload/core/IntegrationTestUploadStarter.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import net.yudichev.jiotty.common.app.ApplicationLifecycleControl;
4 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponent;
5 | import org.apache.commons.cli.CommandLine;
6 |
7 | import javax.inject.Inject;
8 | import java.nio.file.Path;
9 | import java.nio.file.Paths;
10 | import java.util.Optional;
11 | import java.util.concurrent.atomic.AtomicBoolean;
12 | import java.util.concurrent.atomic.AtomicReference;
13 |
14 | import static com.google.common.base.Preconditions.checkNotNull;
15 |
16 | final class IntegrationTestUploadStarter extends BaseLifecycleComponent {
17 | private static final AtomicReference lastFailure = new AtomicReference<>();
18 | private static final AtomicBoolean forgetUploadStateOnShutdown = new AtomicBoolean();
19 | private final Path rootDir;
20 | private final Uploader uploader;
21 | private final ApplicationLifecycleControl applicationLifecycleControl;
22 | private final boolean resume;
23 |
24 | @Inject
25 | IntegrationTestUploadStarter(CommandLine commandLine,
26 | Uploader uploader,
27 | ApplicationLifecycleControl applicationLifecycleControl) {
28 | rootDir = Paths.get(commandLine.getOptionValue('r'));
29 | resume = !commandLine.hasOption('n');
30 | this.uploader = checkNotNull(uploader);
31 | this.applicationLifecycleControl = checkNotNull(applicationLifecycleControl);
32 | }
33 |
34 | public static Optional getLastFailure() {
35 | return Optional.ofNullable(lastFailure.get());
36 | }
37 |
38 | public static void forgetUploadStateOnShutdown() {
39 | forgetUploadStateOnShutdown.set(true);
40 | }
41 |
42 | @Override
43 | protected void doStart() {
44 | uploader.upload(rootDir, resume)
45 | .whenComplete((aVoid, throwable) -> lastFailure.set(throwable))
46 | .whenComplete((aVoid, ignored) -> {
47 | if (forgetUploadStateOnShutdown.getAndSet(false)) {
48 | uploader.forgetUploadState();
49 | }
50 | })
51 | .whenComplete((ignored1, ignored2) -> applicationLifecycleControl.initiateShutdown());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/CloudOperationHelperImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import net.yudichev.jiotty.common.lang.CompletableFutures;
4 | import net.yudichev.jiotty.common.lang.Either;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import javax.inject.Inject;
9 | import java.util.concurrent.CompletableFuture;
10 | import java.util.function.LongConsumer;
11 | import java.util.function.Supplier;
12 |
13 | import static com.google.common.base.Preconditions.checkNotNull;
14 |
15 | final class CloudOperationHelperImpl implements CloudOperationHelper {
16 | private static final Logger logger = LoggerFactory.getLogger(CloudOperationHelperImpl.class);
17 | private final BackingOffRemoteApiExceptionHandler backOffHandler;
18 |
19 | @Inject
20 | CloudOperationHelperImpl(BackingOffRemoteApiExceptionHandler backOffHandler) {
21 | this.backOffHandler = checkNotNull(backOffHandler);
22 | }
23 |
24 | @Override
25 | public CompletableFuture withBackOffAndRetry(String operationName, Supplier> action, LongConsumer backoffEventConsumer) {
26 | return action.get()
27 | .thenApply(value -> {
28 | backOffHandler.reset();
29 | return Either.left(value);
30 | })
31 | .exceptionally(exception -> {
32 | var backoffDelayMs = backOffHandler.handle(operationName, exception);
33 | return Either.right(RetryableFailure.of(exception, backoffDelayMs));
34 | })
35 | .thenCompose(eitherValueOrRetryableFailure -> eitherValueOrRetryableFailure.map(
36 | CompletableFuture::completedFuture,
37 | retryableFailure -> retryableFailure.backoffDelayMs()
38 | .map(backoffDelayMs -> {
39 | logger.debug("Retrying operation '{}' with backoff {}ms", operationName, retryableFailure.backoffDelayMs());
40 | backoffEventConsumer.accept(backoffDelayMs);
41 | return withBackOffAndRetry(operationName, action, backoffEventConsumer);
42 | })
43 | .orElseGet(() -> CompletableFutures.failure(retryableFailure.exception()))
44 | ));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/resources/UploadPane.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/MacPlatformSpecificMenu.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import de.codecentric.centerdevice.MenuToolkit;
4 | import javafx.event.ActionEvent;
5 | import javafx.event.EventHandler;
6 | import javafx.scene.control.MenuBar;
7 | import javafx.scene.control.MenuItem;
8 | import javafx.scene.control.SeparatorMenuItem;
9 | import javafx.scene.input.KeyCodeCombination;
10 |
11 | import javax.inject.Inject;
12 | import java.util.ResourceBundle;
13 |
14 | import static com.google.common.base.Preconditions.checkNotNull;
15 | import static javafx.scene.input.KeyCode.COMMA;
16 | import static javafx.scene.input.KeyCombination.META_DOWN;
17 | import static net.yudichev.googlephotosupload.core.AppGlobals.APP_TITLE;
18 |
19 | final class MacPlatformSpecificMenu implements PlatformSpecificMenu {
20 | private final ResourceBundle resourceBundle;
21 | private MenuItem preferencesMenuItem;
22 | private MenuItem aboutMenuItem;
23 |
24 | @Inject
25 | MacPlatformSpecificMenu(ResourceBundle resourceBundle) {
26 | this.resourceBundle = checkNotNull(resourceBundle);
27 | }
28 |
29 | @Override
30 | public void initialize(MenuBar menuBar) {
31 | var tk = MenuToolkit.toolkit();
32 | var defaultApplicationMenu = tk.createDefaultApplicationMenu(APP_TITLE);
33 |
34 | aboutMenuItem = tk.createAboutMenuItem(APP_TITLE);
35 | defaultApplicationMenu.getItems().set(0, aboutMenuItem);
36 |
37 | preferencesMenuItem = new MenuItem(resourceBundle.getString("menuItemMacPreferences"));
38 | preferencesMenuItem.setAccelerator(new KeyCodeCombination(COMMA, META_DOWN));
39 | defaultApplicationMenu.getItems().add(2, new SeparatorMenuItem());
40 | defaultApplicationMenu.getItems().add(2, preferencesMenuItem);
41 |
42 | defaultApplicationMenu.getItems().add(4, tk.createCloseWindowMenuItem());
43 |
44 | tk.setApplicationMenu(defaultApplicationMenu);
45 | }
46 |
47 | @Override
48 | public void setOnExitAction(EventHandler onExitEventHandler) {
49 | // no dedicated exit menu items on Mac
50 | }
51 |
52 | @Override
53 | public void setOnPreferencesAction(EventHandler onPreferencesEventHandler) {
54 | preferencesMenuItem.setOnAction(onPreferencesEventHandler);
55 | }
56 |
57 | @Override
58 | public void setOnAboutAction(EventHandler onAboutAction) {
59 | aboutMenuItem.setOnAction(onAboutAction);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/resources/ProgressBox.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
46 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/main/resources/PreferencesDialog.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
20 |
21 |
23 |
24 |
25 |
26 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/ResourceBundleModule.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponentModule;
4 | import net.yudichev.jiotty.common.inject.ExposedKeyModule;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import java.io.IOException;
9 | import java.util.List;
10 | import java.util.Locale;
11 | import java.util.ResourceBundle;
12 |
13 | import static com.google.common.base.Preconditions.checkNotNull;
14 |
15 | public final class ResourceBundleModule extends BaseLifecycleComponentModule implements ExposedKeyModule {
16 | private static final Logger logger = LoggerFactory.getLogger(ResourceBundleModule.class);
17 |
18 | public static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle("i18n.Resources", new ResourceBundle.Control() {
19 | @Override
20 | public List getCandidateLocales(String baseName, Locale locale) {
21 | var candidateLocales = super.getCandidateLocales(baseName, locale);
22 | logger.info("Candidate locales: {}", candidateLocales);
23 | return candidateLocales;
24 | }
25 |
26 | @Override
27 | public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
28 | throws IllegalAccessException, InstantiationException, IOException {
29 | var resourceBundle = super.newBundle(baseName, locale, format, loader, reload);
30 | logger.debug("newBundle {}, {} = {}", locale, format, resourceBundle.getLocale());
31 | return resourceBundle;
32 | }
33 |
34 | @Override
35 | public Locale getFallbackLocale(String baseName, Locale locale) {
36 | // coarse support for CHS/CHT
37 | checkNotNull(baseName);
38 | if ("zh".equals(locale.getLanguage())) {
39 | if ("CHS".equals(locale.getVariant().toUpperCase()) || "CHS".equals(locale.getCountry().toUpperCase())) {
40 | return new Locale("zh", "Hans");
41 | }
42 | if ("CHT".equals(locale.getVariant().toUpperCase()) || "CHT".equals(locale.getCountry().toUpperCase())) {
43 | return new Locale("zh", "Hant");
44 | }
45 | }
46 | return super.getFallbackLocale(baseName, locale);
47 | }
48 | });
49 |
50 | static {
51 | logger.info("Using resource bundle locale {}", RESOURCE_BUNDLE.getLocale());
52 | }
53 |
54 | @Override
55 | protected void configure() {
56 | bind(getExposedKey()).toInstance(RESOURCE_BUNDLE);
57 | expose(getExposedKey());
58 | }
59 | }
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/BackingOffRemoteApiExceptionHandlerImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import com.google.api.client.util.BackOff;
4 | import com.google.api.gax.rpc.*;
5 | import com.google.common.collect.ImmutableSet;
6 | import com.google.inject.BindingAnnotation;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | import javax.inject.Inject;
11 | import java.lang.annotation.Retention;
12 | import java.lang.annotation.Target;
13 | import java.util.Optional;
14 | import java.util.Set;
15 |
16 | import static com.google.common.base.Preconditions.checkNotNull;
17 | import static com.google.common.base.Throwables.getCausalChain;
18 | import static java.lang.annotation.ElementType.*;
19 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
20 | import static net.yudichev.jiotty.common.lang.MoreThrowables.asUnchecked;
21 | import static net.yudichev.jiotty.common.lang.MoreThrowables.getAsUnchecked;
22 |
23 | final class BackingOffRemoteApiExceptionHandlerImpl implements BackingOffRemoteApiExceptionHandler {
24 | private static final Logger logger = LoggerFactory.getLogger(BackingOffRemoteApiExceptionHandlerImpl.class);
25 | private final BackOff backOff;
26 | // Unfortunately, the "retryable" flag in most, if not all, all these exceptions is not reliable; some of these
27 | // are marked as not retryable while in reality they are
28 | private final Set> EXCEPTION_TYPES_REQUIRING_BACKOFF = ImmutableSet.of(
29 | ResourceExhaustedException.class,
30 | UnavailableException.class,
31 | DeadlineExceededException.class,
32 | AbortedException.class,
33 | InternalException.class);
34 |
35 | @Inject
36 | BackingOffRemoteApiExceptionHandlerImpl(@Dependency BackOff backOff) {
37 | this.backOff = checkNotNull(backOff);
38 | }
39 |
40 | @Override
41 | public Optional handle(String operationName, Throwable exception) {
42 | return getCausalChain(exception).stream()
43 | .filter(e -> EXCEPTION_TYPES_REQUIRING_BACKOFF.contains(e.getClass()))
44 | .findFirst()
45 | .map(throwable -> {
46 | long backOffMs = getAsUnchecked(backOff::nextBackOffMillis);
47 | logger.debug("Retryable exception performing operation '{}', backing off by waiting for {}ms", operationName, backOffMs, throwable);
48 | asUnchecked(() -> Thread.sleep(backOffMs));
49 | return Optional.of(backOffMs);
50 | })
51 | .orElse(Optional.empty());
52 | }
53 |
54 | @Override
55 | public void reset() {
56 | asUnchecked(backOff::reset);
57 | }
58 |
59 | @BindingAnnotation
60 | @Target({FIELD, PARAMETER, METHOD})
61 | @Retention(RUNTIME)
62 | @interface Dependency {
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/UiMain.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import javafx.application.Platform;
4 | import javafx.scene.image.Image;
5 | import javafx.stage.Stage;
6 | import net.yudichev.googlephotosupload.core.DependenciesModule;
7 | import net.yudichev.googlephotosupload.core.ResourceBundleModule;
8 | import net.yudichev.googlephotosupload.core.UploadPhotosModule;
9 | import net.yudichev.googlephotosupload.ui.Bindings.AuthBrowser;
10 | import net.yudichev.jiotty.common.app.Application;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 | import java.util.concurrent.atomic.AtomicReference;
15 | import java.util.function.Consumer;
16 |
17 | import static com.google.common.base.Preconditions.checkState;
18 | import static net.yudichev.googlephotosupload.core.BuildVersion.buildVersion;
19 | import static net.yudichev.googlephotosupload.ui.SingleInstanceCheck.otherInstanceRunning;
20 | import static net.yudichev.jiotty.common.inject.BindingSpec.annotatedWith;
21 |
22 | public final class UiMain extends javafx.application.Application {
23 | private static final Logger logger = LoggerFactory.getLogger(UiMain.class);
24 | private static final AtomicReference> javafxApplicationResourcesHandler = new AtomicReference<>();
25 |
26 | public static void main(String[] args) {
27 | if (otherInstanceRunning()) {
28 | return;
29 | }
30 | logger.info("Version {}", buildVersion());
31 | logger.info("System properties {}", System.getProperties());
32 | logger.info("Environment {}", System.getenv());
33 | Application.builder()
34 | .addModule(() -> new UiModule(handler -> {
35 | checkState(javafxApplicationResourcesHandler.compareAndSet(null, handler), "can only launch once");
36 | new Thread(() -> launch(args)).start();
37 | }))
38 | .addModule(() -> DependenciesModule.builder()
39 | .withGoogleApiSettingsCustomiser(builder -> builder.setAuthorizationBrowser(annotatedWith(AuthBrowser.class)))
40 | .build())
41 | .addModule(ResourceBundleModule::new)
42 | .addModule(() -> new UploadPhotosModule(1000))
43 | .addModule(UiAuthorizationBrowserModule::new)
44 | .build()
45 | .run();
46 | Platform.exit();
47 | }
48 |
49 | @Override
50 | public void start(Stage primaryStage) {
51 | primaryStage.setMinWidth(500);
52 | primaryStage.setMinHeight(500);
53 | primaryStage.getIcons().add(new Image(getClass().getResource("/Icon1024.png").toString()));
54 | javafxApplicationResourcesHandler.get().accept(JavafxApplicationResources.builder()
55 | .setHostServices(getHostServices())
56 | .setPrimaryStage(primaryStage)
57 | .build());
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/cli/LoggingProgressStatusFactory.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.cli;
2 |
3 | import net.yudichev.googlephotosupload.core.KeyedError;
4 | import net.yudichev.googlephotosupload.core.ProgressStatus;
5 | import net.yudichev.googlephotosupload.core.ProgressStatusFactory;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | import java.time.Duration;
10 | import java.util.Optional;
11 | import java.util.concurrent.locks.Lock;
12 | import java.util.concurrent.locks.ReentrantLock;
13 |
14 | import static net.yudichev.jiotty.common.lang.Locks.inLock;
15 |
16 | final class LoggingProgressStatusFactory implements ProgressStatusFactory {
17 | private static final long BACKOFF_DELAY_MS_BEFORE_NOTICE_APPEARS = Duration.ofMinutes(1).toMillis();
18 | private static final Logger logger = LoggerFactory.getLogger(LoggingProgressStatusFactory.class);
19 |
20 | @Override
21 | public ProgressStatus create(String name, Optional totalCount) {
22 | return new ProgressStatus() {
23 | private final Lock lock = new ReentrantLock();
24 | private int successCount;
25 | private int failureCount;
26 |
27 | @Override
28 | public void updateSuccess(int newValue) {
29 | inLock(lock, () -> {
30 | successCount = newValue;
31 | log();
32 | });
33 | }
34 |
35 | @Override
36 | public void incrementSuccessBy(int increment) {
37 | inLock(lock, () -> {
38 | successCount += increment;
39 | log();
40 | });
41 | }
42 |
43 | @Override
44 | public void onBackoffDelay(long backoffDelayMs) {
45 | if (backoffDelayMs > BACKOFF_DELAY_MS_BEFORE_NOTICE_APPEARS) {
46 | logger.info("Pausing for a long time due to Google imposed request quota...");
47 | }
48 | }
49 |
50 | @Override
51 | public void addFailure(KeyedError keyedError) {
52 | logger.warn("Failure for {}: {}", keyedError.getKey(), keyedError.getError());
53 | inLock(lock, () -> {
54 | failureCount += 1;
55 | log();
56 | });
57 | }
58 |
59 | @Override
60 | public void close(boolean success) {
61 | inLock(lock, () -> logger.info("{}: completed; {} succeeded, {} failed", name, successCount, failureCount));
62 | }
63 |
64 | private void log() {
65 | inLock(lock, () -> totalCount.ifPresentOrElse(
66 | totalCount -> logger.info("{}: progress {}%", name, (successCount + failureCount) * 100 / totalCount),
67 | () -> logger.info("{}: completed {}", name, successCount + failureCount)));
68 | }
69 | };
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/DependenciesModule.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import com.google.inject.AbstractModule;
4 | import com.google.inject.Module;
5 | import net.yudichev.jiotty.common.async.ExecutorModule;
6 | import net.yudichev.jiotty.common.lang.TypedBuilder;
7 | import net.yudichev.jiotty.common.time.TimeModule;
8 | import net.yudichev.jiotty.common.varstore.VarStoreModule;
9 | import net.yudichev.jiotty.connector.google.common.GoogleApiAuthSettings;
10 | import net.yudichev.jiotty.connector.google.photos.GooglePhotosModule;
11 |
12 | import java.nio.file.Path;
13 | import java.nio.file.Paths;
14 | import java.util.function.Consumer;
15 |
16 | import static com.google.common.base.Preconditions.checkNotNull;
17 | import static com.google.common.io.Resources.getResource;
18 | import static net.yudichev.googlephotosupload.core.AppGlobals.APP_SETTINGS_DIR_NAME;
19 | import static net.yudichev.googlephotosupload.core.AppGlobals.APP_TITLE;
20 |
21 | public final class DependenciesModule extends AbstractModule {
22 | private final Consumer googleApiSettingsCustomiser;
23 |
24 | private DependenciesModule(Consumer googleApiSettingsCustomiser) {
25 | this.googleApiSettingsCustomiser = checkNotNull(googleApiSettingsCustomiser);
26 | }
27 |
28 | public static Builder builder() {
29 | return new Builder();
30 | }
31 |
32 | @Override
33 | protected void configure() {
34 | install(new TimeModule());
35 | install(new ExecutorModule());
36 | install(new VarStoreModule(APP_SETTINGS_DIR_NAME));
37 |
38 | var authDataStoreRootDir = Paths.get(System.getProperty("user.home")).resolve("." + APP_SETTINGS_DIR_NAME).resolve("auth");
39 | bind(Restarter.class).to(RestarterImpl.class);
40 | bind(Path.class).annotatedWith(RestarterImpl.GoogleAuthRootDir.class).toInstance(authDataStoreRootDir);
41 |
42 | var googleApiSettingsBuilder = GoogleApiAuthSettings.builder()
43 | .setAuthDataStoreRootDir(authDataStoreRootDir)
44 | .setApplicationName(APP_TITLE)
45 | .setCredentialsUrl(getResource("client_secret.json"));
46 | googleApiSettingsCustomiser.accept(googleApiSettingsBuilder);
47 | install(GooglePhotosModule.builder()
48 | .setSettings(googleApiSettingsBuilder.build())
49 | .build());
50 | }
51 |
52 | public static final class Builder implements TypedBuilder {
53 | private Consumer googleApiSettingsCustomiser = ignored -> {};
54 |
55 | public Builder withGoogleApiSettingsCustomiser(Consumer googleApiSettingsCustomiser) {
56 | this.googleApiSettingsCustomiser = checkNotNull(googleApiSettingsCustomiser);
57 | return this;
58 | }
59 |
60 | @Override
61 | public Module build() {
62 | return new DependenciesModule(googleApiSettingsCustomiser);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/UploadPhotosModule.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import com.google.api.client.util.BackOff;
4 | import com.google.inject.assistedinject.FactoryModuleBuilder;
5 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponentModule;
6 | import net.yudichev.jiotty.common.inject.ExposedKeyModule;
7 |
8 | import javax.inject.Singleton;
9 | import java.util.concurrent.ExecutorService;
10 |
11 | import static net.yudichev.googlephotosupload.core.Bindings.Backpressured;
12 |
13 | @SuppressWarnings("OverlyCoupledClass") // OK for module
14 | public final class UploadPhotosModule extends BaseLifecycleComponentModule implements ExposedKeyModule {
15 | private final int backOffInitialDelayMs;
16 |
17 | public UploadPhotosModule(int backOffInitialDelayMs) {
18 | this.backOffInitialDelayMs = backOffInitialDelayMs;
19 | }
20 |
21 | @Override
22 | protected void configure() {
23 | bind(BuildVersion.class).asEagerSingleton();
24 |
25 | install(new FactoryModuleBuilder()
26 | .implement(StateSaver.class, StateSaverImpl.class)
27 | .build(StateSaverFactory.class));
28 |
29 | bind(ExecutorService.class).annotatedWith(Backpressured.class)
30 | .toProvider(boundLifecycleComponent(BackpressuredExecutorServiceProvider.class));
31 |
32 | bind(DirectoryStructureSupplier.class).to(DirectoryStructureSupplierImpl.class);
33 |
34 | bindConstant().annotatedWith(BackOffProvider.InitialDelayMs.class).to(backOffInitialDelayMs);
35 | bind(BackOff.class).annotatedWith(BackingOffRemoteApiExceptionHandlerImpl.Dependency.class).toProvider(BackOffProvider.class);
36 | bind(BackingOffRemoteApiExceptionHandler.class).to(BackingOffRemoteApiExceptionHandlerImpl.class);
37 | bind(FatalUserCorrectableRemoteApiExceptionHandler.class).to(FatalUserCorrectableRemoteApiExceptionHandlerImpl.class);
38 |
39 | bind(CloudOperationHelper.class).to(CloudOperationHelperImpl.class);
40 | bind(CloudAlbumsProvider.class).to(boundLifecycleComponent(CloudAlbumsProviderImpl.class));
41 |
42 | bind(AlbumManager.class).to(boundLifecycleComponent(AlbumManagerImpl.class));
43 |
44 | bind(UploadStateManager.class).to(UploadStateManagerImpl.class).in(Singleton.class);
45 | bind(AddToAlbumStrategy.class)
46 | .annotatedWith(SelectingAddToAlbumStrategy.WhileCreatingItems.class)
47 | .to(AddToAlbumWhileCreatingStrategy.class)
48 | .in(Singleton.class);
49 | bind(AddToAlbumStrategy.class)
50 | .annotatedWith(SelectingAddToAlbumStrategy.AfterCreatingItemsSorted.class)
51 | .to(AddToAlbumAfterCreatingStrategy.class)
52 | .in(Singleton.class);
53 | bind(AddToAlbumStrategy.class).to(SelectingAddToAlbumStrategy.class);
54 | bind(GooglePhotosUploader.class).to(boundLifecycleComponent(GooglePhotosUploaderImpl.class));
55 |
56 | bind(getExposedKey()).to(UploaderImpl.class);
57 | expose(getExposedKey());
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/UiAuthorizationBrowser.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import javafx.stage.Stage;
4 | import net.yudichev.jiotty.common.app.ApplicationLifecycleControl;
5 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponent;
6 | import net.yudichev.jiotty.connector.google.common.AuthorizationBrowser;
7 |
8 | import javax.inject.Inject;
9 | import javax.inject.Provider;
10 | import java.util.ResourceBundle;
11 |
12 | import static com.google.common.base.Preconditions.checkNotNull;
13 | import static javafx.application.Platform.runLater;
14 | import static javafx.stage.Modality.APPLICATION_MODAL;
15 |
16 | final class UiAuthorizationBrowser extends BaseLifecycleComponent implements AuthorizationBrowser {
17 | static {
18 | System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
19 | }
20 |
21 | private final Provider mainScreenControllerProvider;
22 | private final ApplicationLifecycleControl applicationLifecycleControl;
23 | private final ResourceBundle resourceBundle;
24 | private final DialogFactory dialogFactory;
25 | private Dialog dialog;
26 |
27 | @Inject
28 | UiAuthorizationBrowser(Provider mainScreenControllerProvider,
29 | DialogFactory dialogFactory,
30 | ApplicationLifecycleControl applicationLifecycleControl,
31 | ResourceBundle resourceBundle) {
32 | this.dialogFactory = checkNotNull(dialogFactory);
33 | this.mainScreenControllerProvider = checkNotNull(mainScreenControllerProvider);
34 | this.applicationLifecycleControl = checkNotNull(applicationLifecycleControl);
35 | this.resourceBundle = checkNotNull(resourceBundle);
36 | }
37 |
38 | @Override
39 | public void browse(String url) {
40 | runLater(() -> {
41 | if (dialog == null) {
42 | dialog = dialogFactory.create(
43 | resourceBundle.getString("uiAuthorisationBrowserTitle"),
44 | "LoginDialog.fxml",
45 | this::customizeLoginDialog);
46 | }
47 |
48 | dialog.show();
49 | dialog.controller().load(url);
50 | });
51 | }
52 |
53 | @Override
54 | protected void doStart() {
55 | runLater(() -> {
56 | closeDialog();
57 | mainScreenControllerProvider.get().toFolderSelectionMode();
58 | });
59 | }
60 |
61 | @Override
62 | protected void doStop() {
63 | runLater(this::closeDialog);
64 | }
65 |
66 | private void customizeLoginDialog(Stage dialog) {
67 | dialog.initModality(APPLICATION_MODAL);
68 | dialog.setMinHeight(500);
69 | dialog.setMinWidth(500);
70 | dialog.setOnCloseRequest(event -> {
71 | applicationLifecycleControl.initiateShutdown();
72 | event.consume();
73 | });
74 | }
75 |
76 | private void closeDialog() {
77 | if (dialog != null) {
78 | dialog.close();
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/SelectingAddToAlbumStrategy.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import com.google.inject.BindingAnnotation;
4 | import net.yudichev.jiotty.connector.google.photos.GooglePhotosAlbum;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Provider;
8 | import java.lang.annotation.Retention;
9 | import java.lang.annotation.Target;
10 | import java.nio.file.Path;
11 | import java.util.List;
12 | import java.util.Optional;
13 | import java.util.concurrent.CompletableFuture;
14 | import java.util.function.BiFunction;
15 | import java.util.function.Function;
16 |
17 | import static com.google.common.base.Preconditions.checkNotNull;
18 | import static java.lang.annotation.ElementType.*;
19 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
20 | import static net.yudichev.googlephotosupload.core.AddToAlbumMethod.WHILE_CREATING_ITEMS;
21 |
22 | final class SelectingAddToAlbumStrategy implements AddToAlbumStrategy {
23 | private final PreferencesManager preferencesManager;
24 | private final Provider whileCreatingItemsStrategyProvider;
25 | private final Provider afterCreatingItemsStrategyProvider;
26 |
27 | @Inject
28 | SelectingAddToAlbumStrategy(PreferencesManager preferencesManager,
29 | @WhileCreatingItems Provider whileCreatingItemsStrategyProvider,
30 | @AfterCreatingItemsSorted Provider afterCreatingItemsStrategyProvider) {
31 | this.preferencesManager = checkNotNull(preferencesManager);
32 | this.whileCreatingItemsStrategyProvider = checkNotNull(whileCreatingItemsStrategyProvider);
33 | this.afterCreatingItemsStrategyProvider = checkNotNull(afterCreatingItemsStrategyProvider);
34 | }
35 |
36 | @Override
37 | public CompletableFuture addToAlbum(CompletableFuture> createMediaDataResultsFuture,
38 | Optional googlePhotosAlbum,
39 | ProgressStatus fileProgressStatus,
40 | BiFunction, List, CompletableFuture>> createMediaItems,
41 | Function itemStateRetriever) {
42 | return selectDelegate().addToAlbum(createMediaDataResultsFuture, googlePhotosAlbum, fileProgressStatus, createMediaItems, itemStateRetriever);
43 | }
44 |
45 | private AddToAlbumStrategy selectDelegate() {
46 | return preferencesManager.get().addToAlbumStrategy().orElseThrow(IllegalStateException::new) == WHILE_CREATING_ITEMS ?
47 | whileCreatingItemsStrategyProvider.get() : afterCreatingItemsStrategyProvider.get();
48 | }
49 |
50 | @BindingAnnotation
51 | @Target({FIELD, PARAMETER, METHOD})
52 | @Retention(RUNTIME)
53 | @interface WhileCreatingItems {
54 | }
55 |
56 | @BindingAnnotation
57 | @Target({FIELD, PARAMETER, METHOD})
58 | @Retention(RUNTIME)
59 | @interface AfterCreatingItemsSorted {
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/UpgradeNotificationDialogControllerImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import com.sandec.mdfx.MDFXNode;
4 | import javafx.event.ActionEvent;
5 | import javafx.scene.control.Label;
6 | import javafx.scene.control.ScrollPane;
7 | import javafx.scene.control.TitledPane;
8 |
9 | import javax.inject.Inject;
10 | import javax.inject.Provider;
11 | import java.util.List;
12 | import java.util.ResourceBundle;
13 |
14 | import static com.google.common.base.Preconditions.checkNotNull;
15 | import static java.lang.System.lineSeparator;
16 | import static net.yudichev.googlephotosupload.core.BuildVersion.buildVersion;
17 |
18 | public final class UpgradeNotificationDialogControllerImpl implements UpgradeNotificationDialogController {
19 | private final ResourceBundle resourceBundle;
20 | private final Provider javafxApplicationResourcesProvider;
21 | public Label label;
22 | public TitledPane releaseNotesPane;
23 | public ScrollPane releaseNotesScrollPane;
24 | private GithubRevision highestAvailableVersion;
25 | private Runnable dismissAction;
26 | private Runnable ignoreVersionAction;
27 |
28 | @Inject
29 | UpgradeNotificationDialogControllerImpl(ResourceBundle resourceBundle,
30 | Provider javafxApplicationResourcesProvider) {
31 | this.resourceBundle = checkNotNull(resourceBundle);
32 | this.javafxApplicationResourcesProvider = checkNotNull(javafxApplicationResourcesProvider);
33 | }
34 |
35 | @Override
36 | public void initialise(List orderedNewerRevisions, Runnable dismissAction, Runnable ignoreVersionAction, Runnable dialogResizeAction) {
37 | highestAvailableVersion = orderedNewerRevisions.get(0);
38 | this.dismissAction = checkNotNull(dismissAction);
39 | this.ignoreVersionAction = checkNotNull(ignoreVersionAction);
40 | label.setText(String.format(resourceBundle.getString("upgradeDialogText"), buildVersion(), highestAvailableVersion.tag_name()));
41 |
42 | releaseNotesPane.setVisible(true);
43 | var builder = new StringBuilder(1024);
44 | orderedNewerRevisions.stream()
45 | .filter(githubRevision -> githubRevision.body().isPresent())
46 | .forEach(revision -> builder
47 | .append("### ").append(revision.tag_name()).append(lineSeparator())
48 | .append(revision.body().get()).append(lineSeparator()));
49 |
50 | releaseNotesScrollPane.setContent(new MDFXNode(builder.toString()));
51 | releaseNotesPane.heightProperty().addListener((obs, oldHeight, newHeight) -> dialogResizeAction.run());
52 | }
53 |
54 | public void onDownloadButtonAction(ActionEvent actionEvent) {
55 | javafxApplicationResourcesProvider.get().hostServices().showDocument(highestAvailableVersion.html_url());
56 | actionEvent.consume();
57 | }
58 |
59 | public void onAskLaterButtonAction(ActionEvent actionEvent) {
60 | dismissAction.run();
61 | actionEvent.consume();
62 | }
63 |
64 | public void onIgnoreButtonAction(ActionEvent actionEvent) {
65 | ignoreVersionAction.run();
66 | actionEvent.consume();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
25 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/ui/UserInterface.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.ui;
2 |
3 | import javafx.scene.Parent;
4 | import javafx.scene.Scene;
5 | import net.yudichev.jiotty.common.app.ApplicationLifecycleControl;
6 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponent;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | import javax.inject.Inject;
11 | import javax.inject.Provider;
12 | import java.util.concurrent.CountDownLatch;
13 | import java.util.concurrent.TimeUnit;
14 | import java.util.function.Consumer;
15 |
16 | import static com.google.common.base.Preconditions.checkNotNull;
17 | import static com.google.common.base.Preconditions.checkState;
18 | import static net.yudichev.googlephotosupload.core.AppGlobals.APP_TITLE;
19 | import static net.yudichev.jiotty.common.lang.MoreThrowables.getAsUnchecked;
20 |
21 | final class UserInterface extends BaseLifecycleComponent implements Provider {
22 | private static final Logger logger = LoggerFactory.getLogger(UserInterface.class);
23 |
24 | private final Consumer> javafxApplicationResourcesHandler;
25 | private final FxmlContainerFactory fxmlContainerFactory;
26 | private final ApplicationLifecycleControl applicationLifecycleControl;
27 | private volatile JavafxApplicationResources javafxApplicationResources;
28 |
29 | @Inject
30 | UserInterface(Consumer> javafxApplicationResourcesHandler,
31 | FxmlContainerFactory fxmlContainerFactory,
32 | ApplicationLifecycleControl applicationLifecycleControl) {
33 | this.javafxApplicationResourcesHandler = checkNotNull(javafxApplicationResourcesHandler);
34 | this.fxmlContainerFactory = checkNotNull(fxmlContainerFactory);
35 | this.applicationLifecycleControl = checkNotNull(applicationLifecycleControl);
36 | }
37 |
38 | @Override
39 | public JavafxApplicationResources get() {
40 | return checkNotNull(javafxApplicationResources, "primaryStage is not set");
41 | }
42 |
43 | @Override
44 | protected void doStart() {
45 | if (javafxApplicationResources == null) {
46 | var initLatch = new CountDownLatch(1);
47 | javafxApplicationResourcesHandler.accept(javafxApplicationResources -> {
48 | Thread.currentThread().setUncaughtExceptionHandler((thread, throwable) -> {
49 | logger.error("Unhandled exception", throwable);
50 | applicationLifecycleControl.initiateShutdown();
51 | });
52 |
53 | this.javafxApplicationResources = javafxApplicationResources;
54 | var primaryStage = javafxApplicationResources.primaryStage();
55 |
56 | var fxmlContainer = fxmlContainerFactory.create("MainScreen.fxml");
57 | Parent parent = fxmlContainer.root();
58 | primaryStage.setScene(new Scene(parent));
59 | primaryStage.setTitle(APP_TITLE);
60 | primaryStage.show();
61 | primaryStage.setOnHiding(e -> applicationLifecycleControl.initiateShutdown());
62 | initLatch.countDown();
63 | });
64 | logger.info("Waiting for 10 seconds until UI is initialized");
65 | checkState(getAsUnchecked(() -> initLatch.await(10, TimeUnit.SECONDS)), "UI was not initialized in 10 seconds");
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/net/yudichev/googlephotosupload/core/CloudAlbumsProviderImpl.java:
--------------------------------------------------------------------------------
1 | package net.yudichev.googlephotosupload.core;
2 |
3 | import net.yudichev.jiotty.common.inject.BaseLifecycleComponent;
4 | import net.yudichev.jiotty.connector.google.photos.GooglePhotosAlbum;
5 | import net.yudichev.jiotty.connector.google.photos.GooglePhotosClient;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | import javax.inject.Inject;
10 | import javax.inject.Provider;
11 | import java.util.*;
12 | import java.util.concurrent.CompletableFuture;
13 | import java.util.concurrent.ExecutorService;
14 |
15 | import static com.google.common.base.Preconditions.checkNotNull;
16 | import static java.util.stream.Collectors.groupingBy;
17 | import static java.util.stream.Collectors.toList;
18 | import static net.yudichev.googlephotosupload.core.Bindings.Backpressured;
19 |
20 | final class CloudAlbumsProviderImpl extends BaseLifecycleComponent implements CloudAlbumsProvider {
21 | private static final Logger logger = LoggerFactory.getLogger(CloudAlbumsProviderImpl.class);
22 | private final CloudOperationHelper cloudOperationHelper;
23 | private final GooglePhotosClient googlePhotosClient;
24 | private final Provider executorServiceProvider;
25 | private final ProgressStatusFactory progressStatusFactory;
26 | private final ResourceBundle resourceBundle;
27 |
28 | private volatile ExecutorService executorService;
29 |
30 | @Inject
31 | CloudAlbumsProviderImpl(CloudOperationHelper cloudOperationHelper,
32 | GooglePhotosClient googlePhotosClient,
33 | @SuppressWarnings("BoundedWildcard") @Backpressured Provider executorServiceProvider,
34 | ProgressStatusFactory progressStatusFactory,
35 | ResourceBundle resourceBundle) {
36 | this.cloudOperationHelper = checkNotNull(cloudOperationHelper);
37 | this.googlePhotosClient = checkNotNull(googlePhotosClient);
38 | this.executorServiceProvider = executorServiceProvider;
39 | this.progressStatusFactory = checkNotNull(progressStatusFactory);
40 | this.resourceBundle = checkNotNull(resourceBundle);
41 | }
42 |
43 | @Override
44 | public CompletableFuture